diff --git a/Assets/Resources/UIToolkit/Button/UTKCheckBox.uss b/Assets/Resources/UIToolkit/Button/UTKCheckBox.uss
index 913b5037..f62f89a4 100644
--- a/Assets/Resources/UIToolkit/Button/UTKCheckBox.uss
+++ b/Assets/Resources/UIToolkit/Button/UTKCheckBox.uss
@@ -55,7 +55,7 @@
width: 14px;
height: 14px;
font-size: 12px;
- color: var(--color-base-01);
+ color: var(--color-text-primary);
-unity-text-align: middle-center;
-unity-font-style: bold;
opacity: 0;
diff --git a/Assets/Resources/UIToolkit/Button/UTKToggle.uss b/Assets/Resources/UIToolkit/Button/UTKToggle.uss
index a167961a..fafadd04 100644
--- a/Assets/Resources/UIToolkit/Button/UTKToggle.uss
+++ b/Assets/Resources/UIToolkit/Button/UTKToggle.uss
@@ -50,6 +50,7 @@
.utk-toggle > .unity-toggle__input > .unity-toggle__checkmark {
width: 14px;
height: 14px;
+ border-width: 0;
border-radius: var(--radius-full);
background-color: var(--color-text-secondary);
-unity-background-image-tint-color: transparent;
diff --git a/Assets/Resources/UIToolkit/Input/UTKBoundsField.uss b/Assets/Resources/UIToolkit/Input/UTKBoundsField.uss
index 1ba59520..4ce96dec 100644
--- a/Assets/Resources/UIToolkit/Input/UTKBoundsField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKBoundsField.uss
@@ -12,6 +12,8 @@
.utk-boundsfield {
flex-direction: row;
align-items: flex-start;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -101,6 +103,35 @@
Disabled State
=================================== */
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-boundsfield--error .unity-float-field > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-boundsfield--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-boundsfield__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
+/* ===================================
+ Disabled State
+ =================================== */
+
+.utk-boundsfield--disabled {
+ cursor: arrow;
+}
+
.utk-boundsfield--disabled > .unity-label {
color: var(--color-text-disabled);
}
@@ -108,4 +139,9 @@
.utk-boundsfield--disabled .unity-float-field > .unity-base-text-field__input {
background-color: var(--color-btn-disabled);
color: var(--color-text-disabled);
+ cursor: arrow;
+}
+
+.utk-boundsfield--disabled .unity-float-field > .unity-base-text-field__input:focus {
+ border-color: var(--color-border);
}
diff --git a/Assets/Resources/UIToolkit/Input/UTKDoubleField.uss b/Assets/Resources/UIToolkit/Input/UTKDoubleField.uss
index 823eac10..7dcfba95 100644
--- a/Assets/Resources/UIToolkit/Input/UTKDoubleField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKDoubleField.uss
@@ -12,6 +12,8 @@
.utk-double-field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -55,6 +57,28 @@
border-color: var(--color-border-focus);
}
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-double-field--error > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-double-field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-double-field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ padding-left: 68px;
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
/* ===================================
Disabled State
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKFloatField.uss b/Assets/Resources/UIToolkit/Input/UTKFloatField.uss
index c25d4113..08de3f6a 100644
--- a/Assets/Resources/UIToolkit/Input/UTKFloatField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKFloatField.uss
@@ -12,6 +12,8 @@
.utk-float-field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -55,6 +57,28 @@
border-color: var(--color-border-focus);
}
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-float-field--error > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-float-field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-float-field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ padding-left: 68px;
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
/* ===================================
Disabled State
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKFloatStepper.uss b/Assets/Resources/UIToolkit/Input/UTKFloatStepper.uss
index c3f339f2..243c0485 100644
--- a/Assets/Resources/UIToolkit/Input/UTKFloatStepper.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKFloatStepper.uss
@@ -12,7 +12,9 @@
.utk-number-stepper {
flex-direction: row;
align-items: stretch;
- height: var(--size-input-height);
+ flex-wrap: wrap;
+ height: auto;
+ min-height: var(--size-input-height);
}
@@ -154,3 +156,29 @@
.utk-number-stepper--disabled .utk-number-stepper__btn:active {
background-color: var(--color-btn-disabled);
}
+
+
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-number-stepper--error .utk-number-stepper__text-input {
+ border-top-color: var(--color-border-error);
+ border-bottom-color: var(--color-border-error);
+ border-left-color: var(--color-border-error);
+}
+
+.utk-number-stepper--error .utk-number-stepper__btn {
+ border-top-color: var(--color-border-error);
+ border-right-color: var(--color-border-error);
+ border-bottom-color: var(--color-border-error);
+}
+
+/* Error Message Label */
+.utk-number-stepper__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
diff --git a/Assets/Resources/UIToolkit/Input/UTKInputField.uss b/Assets/Resources/UIToolkit/Input/UTKInputField.uss
index a63169f6..0a5c8fa0 100644
--- a/Assets/Resources/UIToolkit/Input/UTKInputField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKInputField.uss
@@ -12,6 +12,8 @@
.utk-input {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -99,6 +101,16 @@
color: var(--color-state-error);
}
+/* Error Message Label */
+.utk-input__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ padding-left: 68px;
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
/* ===================================
Variants
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKIntStepper.uss b/Assets/Resources/UIToolkit/Input/UTKIntStepper.uss
index aea760b1..9f370b63 100644
--- a/Assets/Resources/UIToolkit/Input/UTKIntStepper.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKIntStepper.uss
@@ -12,7 +12,9 @@
.utk-number-stepper {
flex-direction: row;
align-items: stretch;
- height: var(--size-input-height);
+ flex-wrap: wrap;
+ height: auto;
+ min-height: var(--size-input-height);
}
@@ -154,3 +156,28 @@
.utk-number-stepper--disabled .utk-number-stepper__btn:active {
background-color: var(--color-btn-disabled);
}
+
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-number-stepper--error .utk-number-stepper__text-input {
+ border-top-color: var(--color-border-error);
+ border-bottom-color: var(--color-border-error);
+ border-left-color: var(--color-border-error);
+}
+
+.utk-number-stepper--error .utk-number-stepper__btn {
+ border-top-color: var(--color-border-error);
+ border-right-color: var(--color-border-error);
+ border-bottom-color: var(--color-border-error);
+}
+
+/* Error Message Label */
+.utk-number-stepper__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
diff --git a/Assets/Resources/UIToolkit/Input/UTKIntegerField.uss b/Assets/Resources/UIToolkit/Input/UTKIntegerField.uss
index b198a0c2..42055b47 100644
--- a/Assets/Resources/UIToolkit/Input/UTKIntegerField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKIntegerField.uss
@@ -12,6 +12,8 @@
.utk-integer-field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -55,6 +57,28 @@
border-color: var(--color-border-focus);
}
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-integer-field--error > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-integer-field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-integer-field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ padding-left: 68px;
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
/* ===================================
Disabled State
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKLongField.uss b/Assets/Resources/UIToolkit/Input/UTKLongField.uss
index b525333d..2301560c 100644
--- a/Assets/Resources/UIToolkit/Input/UTKLongField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKLongField.uss
@@ -12,6 +12,8 @@
.utk-long-field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -55,6 +57,27 @@
border-color: var(--color-border-focus);
}
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-long-field--error > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-long-field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-long-field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ padding-left: 68px;
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+}
+
/* ===================================
Disabled State
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKRectField.uss b/Assets/Resources/UIToolkit/Input/UTKRectField.uss
index d8f41a14..d5fb5d7e 100644
--- a/Assets/Resources/UIToolkit/Input/UTKRectField.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKRectField.uss
@@ -12,6 +12,8 @@
.utk-rectfield {
flex-direction: row;
align-items: flex-start;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -92,6 +94,35 @@
Disabled State
=================================== */
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-rectfield--error .unity-float-field > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-rectfield--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-rectfield__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
+/* ===================================
+ Disabled State
+ =================================== */
+
+.utk-rectfield--disabled {
+ cursor: arrow;
+}
+
.utk-rectfield--disabled > .unity-label {
color: var(--color-text-disabled);
}
@@ -99,4 +130,9 @@
.utk-rectfield--disabled .unity-float-field > .unity-base-text-field__input {
background-color: var(--color-btn-disabled);
color: var(--color-text-disabled);
+ cursor: arrow;
+}
+
+.utk-rectfield--disabled .unity-float-field > .unity-base-text-field__input:focus {
+ border-color: var(--color-border);
}
diff --git a/Assets/Resources/UIToolkit/Input/UTKVector2Field.uss b/Assets/Resources/UIToolkit/Input/UTKVector2Field.uss
index 7c8e6eee..5ebcc7dc 100644
--- a/Assets/Resources/UIToolkit/Input/UTKVector2Field.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKVector2Field.uss
@@ -12,6 +12,8 @@
.utk-vector2-field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
margin-right: 0;
}
@@ -98,6 +100,27 @@
border-color: var(--color-border-focus);
}
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-vector2-field--error .unity-float-field > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-vector2-field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-vector2-field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
/* ===================================
Disabled State
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKVector3Field.uss b/Assets/Resources/UIToolkit/Input/UTKVector3Field.uss
index b2e21c9d..bf43a95e 100644
--- a/Assets/Resources/UIToolkit/Input/UTKVector3Field.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKVector3Field.uss
@@ -12,6 +12,8 @@
.utk-vector3-field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
margin-right: 0;
}
@@ -93,6 +95,27 @@
border-color: var(--color-border-focus);
}
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-vector3-field--error .unity-float-field > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-vector3-field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-vector3-field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
/* ===================================
Disabled State
=================================== */
diff --git a/Assets/Resources/UIToolkit/Input/UTKVector4Field.uss b/Assets/Resources/UIToolkit/Input/UTKVector4Field.uss
index 6e1a4823..0fd804b7 100644
--- a/Assets/Resources/UIToolkit/Input/UTKVector4Field.uss
+++ b/Assets/Resources/UIToolkit/Input/UTKVector4Field.uss
@@ -12,6 +12,8 @@
.utk-vector4field {
flex-direction: row;
align-items: center;
+ flex-wrap: wrap;
+ height: auto;
}
/* ===================================
@@ -79,6 +81,35 @@
Disabled State
=================================== */
+/* ===================================
+ Error State
+ =================================== */
+
+.utk-vector4field--error .unity-float-field > .unity-base-text-field__input {
+ border-color: var(--color-border-error);
+}
+
+.utk-vector4field--error > .unity-label {
+ color: var(--color-state-error);
+}
+
+/* Error Message Label */
+.utk-vector4field__error-message {
+ width: 100%;
+ margin-top: var(--space-xs);
+ font-size: var(--font-size-caption);
+ color: var(--color-state-error);
+ white-space: normal;
+}
+
+/* ===================================
+ Disabled State
+ =================================== */
+
+.utk-vector4field--disabled {
+ cursor: arrow;
+}
+
.utk-vector4field--disabled > .unity-label {
color: var(--color-text-disabled);
}
@@ -86,4 +117,9 @@
.utk-vector4field--disabled .unity-float-field > .unity-base-text-field__input {
background-color: var(--color-btn-disabled);
color: var(--color-text-disabled);
+ cursor: arrow;
+}
+
+.utk-vector4field--disabled .unity-float-field > .unity-base-text-field__input:focus {
+ border-color: var(--color-border);
}
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKBoundsFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKBoundsFieldSample.uxml
index 111c377c..6a447474 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKBoundsFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKBoundsFieldSample.uxml
@@ -14,6 +14,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKDoubleFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKDoubleFieldSample.uxml
index 7ac3ea1a..74f8b62b 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKDoubleFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKDoubleFieldSample.uxml
@@ -22,6 +22,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKFloatFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatFieldSample.uxml
index eb383a74..048c527f 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKFloatFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatFieldSample.uxml
@@ -22,6 +22,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uss b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uss
new file mode 100644
index 00000000..cce8962b
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uss
@@ -0,0 +1,10 @@
+/*
+ * ===================================
+ * UTKFloatStepperSample.uss
+ * Float Stepper Sample Specific Styles
+ * ===================================
+ */
+
+.utk-sample-stepper {
+ width: 150px;
+}
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uss.meta b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uss.meta
new file mode 100644
index 00000000..97140d97
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f2dcdd47a630b394ea19ea8b91f61d01
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uxml
new file mode 100644
index 00000000..f4fd2df0
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uxml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uxml.meta b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uxml.meta
new file mode 100644
index 00000000..16ce2ee5
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKFloatStepperSample.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 908e94035af6fc5419bdf2246bc903fd
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKInputFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKInputFieldSample.uxml
index c5595968..1518b753 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKInputFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKInputFieldSample.uxml
@@ -38,6 +38,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKIntStepperSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKIntStepperSample.uxml
index 22d83ca7..1696c8e7 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKIntStepperSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKIntStepperSample.uxml
@@ -38,6 +38,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKIntegerFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKIntegerFieldSample.uxml
index ace0b168..32b4a085 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKIntegerFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKIntegerFieldSample.uxml
@@ -22,6 +22,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKLongFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKLongFieldSample.uxml
index b469e591..a2aee3d3 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKLongFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKLongFieldSample.uxml
@@ -22,6 +22,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKRectFieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKRectFieldSample.uxml
index 4226c987..b1d8e37c 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKRectFieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKRectFieldSample.uxml
@@ -14,6 +14,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKVector2FieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKVector2FieldSample.uxml
index df329325..f623fcfb 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKVector2FieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKVector2FieldSample.uxml
@@ -14,6 +14,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKVector3FieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKVector3FieldSample.uxml
index ce6d8abe..b40ae70a 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKVector3FieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKVector3FieldSample.uxml
@@ -14,6 +14,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Input/UTKVector4FieldSample.uxml b/Assets/Resources/UIToolkit/Sample/Input/UTKVector4FieldSample.uxml
index 4d5aa051..37c3f142 100644
--- a/Assets/Resources/UIToolkit/Sample/Input/UTKVector4FieldSample.uxml
+++ b/Assets/Resources/UIToolkit/Sample/Input/UTKVector4FieldSample.uxml
@@ -14,6 +14,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Style/UTKDefaultStyle.uss b/Assets/Resources/UIToolkit/Style/UTKDefaultStyle.uss
index aea2a4ef..c0207033 100644
--- a/Assets/Resources/UIToolkit/Style/UTKDefaultStyle.uss
+++ b/Assets/Resources/UIToolkit/Style/UTKDefaultStyle.uss
@@ -258,7 +258,8 @@ Textfield 항목 스타일
.unity-base-text-field {
border-radius: 4px;
- height: 24px;
+ /* height: 24px; */
+ height: auto;
width: auto;
margin-top: 0;
margin-bottom: 0;
diff --git a/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss b/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss
index 76d3b5a3..ff4be6ae 100644
--- a/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss
+++ b/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss
@@ -50,20 +50,13 @@
--color-blue-10: #062F4A;
--color-blue-11: #001F33;
- /* ===================================
- Accent Colors - Blue Light (color-blue-light-*)
- 라이트 테마 선택 항목용 밝은 파란색
- =================================== */
- --color-blue-light-01: #B3D7FF;
- --color-blue-light-02: #8EC5FF;
-
/* ===================================
Accent Colors - Red (color-red-*)
=================================== */
--color-red-01: #F14C4C;
--color-red-02: #C74E39;
--color-red-03: #FF0000;
- --color-red-04: #264F78;
+ --color-red-04: #782626;
/* ===================================
Accent Colors - Green (color-green-*)
@@ -170,10 +163,10 @@
=================================== */
--color-calendar-sunday: var(--color-red-01);
--color-calendar-saturday: var(--color-blue-02);
- --color-calendar-today: var(--color-seti-orange);
+ --color-calendar-today: var(--color-green-02);
--color-calendar-selected: var(--color-blue-06);
- --color-calendar-range: rgba(66, 133, 244, 0.25);
- --color-calendar-range-hover: rgba(66, 133, 244, 0.35);
+ --color-calendar-range: var(--color-blue-06);
+ --color-calendar-range-hover: var(--color-blue-07);
/* ===================================
Semantic Colors - State (color-state-*)
diff --git a/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss b/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss
index 13a1c4f2..7c0ba94c 100644
--- a/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss
+++ b/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss
@@ -41,67 +41,64 @@
/* ===================================
Accent Colors - Blue (color-blue-*)
- 라이트 테마에서는 약간 더 어두운 톤 사용
+ 라이트 테마에서는 검정 텍스트가 잘 보이도록 밝은 톤 사용
=================================== */
- --color-blue-01: #0066CC;
- --color-blue-02: #0078D4;
- --color-blue-03: #106EBE;
- --color-blue-04: #005A9E;
- --color-blue-05: #004578;
- --color-blue-06: #003F6E;
- --color-blue-07: #003462;
- --color-blue-08: #002952;
- --color-blue-09: #001F3F;
- --color-blue-10: #001530;
- --color-blue-11: #000B1A;
-
- /* ===================================
- Accent Colors - Blue Light (color-blue-light-*)
- 라이트 테마 선택 항목용 밝은 파란색
- =================================== */
- --color-blue-light-01: #B3D7FF;
- --color-blue-light-02: #8EC5FF;
+ --color-blue-01: #DCEEFB;
+ --color-blue-02: #B3D7F5;
+ --color-blue-03: #8AC0EF;
+ --color-blue-04: #62A8E8;
+ --color-blue-05: #3A90E0;
+ --color-blue-06: #2979C9;
+ --color-blue-07: #1A66B3;
+ --color-blue-08: #0D539C;
+ --color-blue-09: #044185;
+ --color-blue-10: #00306E;
+ --color-blue-11: #002057;
/* ===================================
Accent Colors - Red (color-red-*)
=================================== */
- --color-red-01: #D32F2F;
- --color-red-02: #B71C1C;
- --color-red-03: #E53935;
- --color-red-04: #C62828;
+ --color-red-01: #F5A3A3;
+ --color-red-02: #EF7B7B;
+ --color-red-03: #F28C8C;
+ --color-red-04: #E86A6A;
+ --color-red-05: #F14C4C;
+ --color-red-06: #C74E39;
+ --color-red-07: #FF0000;
+ --color-red-08: #782626;
/* ===================================
Accent Colors - Green (color-green-*)
=================================== */
- --color-green-01: #388E3C;
- --color-green-02: #2E7D32;
- --color-green-03: #1B5E20;
- --color-green-04: #4CAF50;
- --color-green-05: #43A047;
+ --color-green-01: #A5D6A7;
+ --color-green-02: #81C784;
+ --color-green-03: #66BB6A;
+ --color-green-04: #B8E6B9;
+ --color-green-05: #98D99A;
/* ===================================
Accent Colors - Purple (color-purple-*)
=================================== */
- --color-purple-01: #7B1FA2;
- --color-purple-02: #6A1B9A;
- --color-purple-03: #9C27B0;
- --color-purple-04: #8E24AA;
+ --color-purple-01: #CE93D8;
+ --color-purple-02: #BA68C8;
+ --color-purple-03: #D1A3DB;
+ --color-purple-04: #C280CE;
/* ===================================
Accent Colors - Yellow (color-yellow-*)
=================================== */
- --color-yellow-01: #F9A825;
- --color-yellow-02: #F57F17;
- --color-yellow-03: #FF8F00;
- --color-yellow-04: #FF6F00;
- --color-yellow-05: #FFD600;
+ --color-yellow-01: #FFE082;
+ --color-yellow-02: #FFD54F;
+ --color-yellow-03: #FFCA28;
+ --color-yellow-04: #FFC107;
+ --color-yellow-05: #FFF176;
/* ===================================
Accent Colors - Orange (color-orange-*)
=================================== */
- --color-orange-01: #E65100;
- --color-orange-02: #EF6C00;
- --color-orange-03: #F57C00;
+ --color-orange-01: #FFAB91;
+ --color-orange-02: #FF9A76;
+ --color-orange-03: #FFB74D;
/* ===================================
Gray Colors (color-gray-*)
@@ -113,17 +110,17 @@
/* ===================================
Seti Colors (color-seti-*)
=================================== */
- --color-seti-blue: #1976D2;
- --color-seti-green: #689F38;
- --color-seti-orange: #E65100;
- --color-seti-pink: #D81B60;
- --color-seti-red: #C62828;
- --color-seti-steel: #455A64;
- --color-seti-yellow: #F9A825;
- --color-seti-purple: #7B1FA2;
- --color-seti-ignore: #78909C;
+ --color-seti-blue: #64B5F6;
+ --color-seti-green: #AED581;
+ --color-seti-orange: #FFAB91;
+ --color-seti-pink: #F48FB1;
+ --color-seti-red: #EF9A9A;
+ --color-seti-steel: #90A4AE;
+ --color-seti-yellow: #FFE082;
+ --color-seti-purple: #CE93D8;
+ --color-seti-ignore: #B0BEC5;
--color-seti-white: #ECEFF1;
- --color-seti-gray: #607D8B;
+ --color-seti-gray: #90A4AE;
/* ===================================
Semantic Colors - Text (color-text-*)
@@ -143,7 +140,7 @@
=================================== */
--color-bg-base: var(--color-base-01);
--color-bg-secondary: var(--color-base-03);
- --color-bg-tertiary: var(--color-base-08);
+ --color-bg-tertiary: var(--color-base-06);
--color-bg-elevated: var(--color-base-02);
--color-bg-modal: var(--color-base-02);
--color-bg-panel: var(--color-base-01);
@@ -167,35 +164,35 @@
--color-btn-hover: var(--color-base-05);
--color-btn-pressed: var(--color-base-06);
--color-btn-disabled: var(--color-base-03);
- --color-btn-primary: var(--color-blue-02);
- --color-btn-primary-hover: var(--color-blue-03);
+ --color-btn-primary: var(--color-blue-03);
+ --color-btn-primary-hover: var(--color-blue-04);
--color-btn-secondary: var(--color-base-05);
--color-btn-secondary-hover: var(--color-base-06);
/* ===================================
Semantic Colors - Calendar (color-calendar-*)
=================================== */
- --color-calendar-sunday: var(--color-red-01);
- --color-calendar-saturday: var(--color-blue-02);
- --color-calendar-today: var(--color-orange-02);
+ --color-calendar-sunday: var(--color-red-05);
+ --color-calendar-saturday: var(--color-blue-05);
+ --color-calendar-today: var(--color-green-02);
--color-calendar-selected: var(--color-blue-02);
- --color-calendar-range: rgba(66, 133, 244, 0.15);
- --color-calendar-range-hover: rgba(66, 133, 244, 0.25);
+ --color-calendar-range: var(--color-blue-02);
+ --color-calendar-range-hover: var(--color-blue-03);
/* ===================================
Semantic Colors - State (color-state-*)
=================================== */
- --color-state-success: var(--color-green-01);
+ --color-state-success: var(--color-green-02);
--color-state-warning: var(--color-orange-02);
- --color-state-error: var(--color-red-01);
- --color-state-info: var(--color-blue-02);
+ --color-state-error: var(--color-red-03);
+ --color-state-info: var(--color-blue-04);
/* ===================================
Semantic Colors - Vector (color-vector-*)
=================================== */
- --color-vector-x: rgba(200, 60, 60, 1);
- --color-vector-y: rgba(60, 160, 60, 1);
- --color-vector-z: rgba(60, 60, 200, 1);
+ --color-vector-x: rgba(239, 130, 130, 1);
+ --color-vector-y: rgba(130, 210, 130, 1);
+ --color-vector-z: rgba(130, 150, 239, 1);
/* ===================================
Semantic Colors - Scrollbar (color-scroller-*)
@@ -216,8 +213,8 @@
=================================== */
--color-collection-item: rgba(255, 255, 255, 0);
--color-collection-item-hover: var(--color-base-04);
- --color-collection-item-selected: var(--color-blue-light-01);
- --color-collection-item-selected-hover: var(--color-blue-light-02);
+ --color-collection-item-selected: var(--color-blue-04);
+ --color-collection-item-selected-hover: var(--color-blue-05);
/* ===================================
Semantic Colors - Tree Checkmark (color-tree-checkmark-*)
diff --git a/Assets/Resources/UIToolkit/Window/UTKTreeListWindow.uxml b/Assets/Resources/UIToolkit/Window/UTKTreeListWindow.uxml
index 24611e2f..9d5d575b 100644
--- a/Assets/Resources/UIToolkit/Window/UTKTreeListWindow.uxml
+++ b/Assets/Resources/UIToolkit/Window/UTKTreeListWindow.uxml
@@ -6,7 +6,7 @@
-
+
diff --git a/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs b/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs
index 58c43407..fa49d7e9 100644
--- a/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs
+++ b/Assets/Sample/UIToolkit/UTKPropertyListWindowSample.cs
@@ -96,6 +96,10 @@ namespace UVC.Sample.UIToolkit
_propertyWindow.OnPropertyButtonClicked += (id, actionName) =>
{
Debug.Log($"Button Clicked: {id} - Action: {actionName}");
+ if(id == "string_with_btn" && actionName == "string_action")
+ {
+ _propertyWindow.SetPropertyValue("string_with_btn", "Button clicked!");
+ }
};
// 샘플 데이터 생성
diff --git a/Assets/Sample/UIToolkit/UTKStyleGuideSample.Input.cs b/Assets/Sample/UIToolkit/UTKStyleGuideSample.Input.cs
index 9d4aad14..2e8d4dd3 100644
--- a/Assets/Sample/UIToolkit/UTKStyleGuideSample.Input.cs
+++ b/Assets/Sample/UIToolkit/UTKStyleGuideSample.Input.cs
@@ -15,6 +15,45 @@ public partial class UTKStyleGuideSample
private void InitializeInputFieldSample(VisualElement root)
{
+ // Validation - Email
+ var emailInput = root.Q("input-validation-email");
+ if (emailInput != null)
+ {
+ emailInput.ErrorMessage = "올바른 이메일 형식이 아닙니다.";
+ emailInput.Validation = () => emailInput.Value.Contains("@") && emailInput.Value.Contains(".");
+ }
+
+ // Validation - Required
+ var requiredInput = root.Q("input-validation-required");
+ if (requiredInput != null)
+ {
+ requiredInput.ErrorMessage = "이름은 필수 항목입니다.";
+ requiredInput.Validation = () => !string.IsNullOrWhiteSpace(requiredInput.Value);
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var emailValid = emailInput?.Validate() ?? true;
+ var nameValid = requiredInput?.Validate() ?? true;
+ var allValid = emailValid && nameValid;
+ Debug.Log($"Validation: {(allValid ? "All Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ emailInput?.ClearError();
+ requiredInput?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 입력 필드
var input = new UTKInputField();
@@ -27,10 +66,6 @@ var password = new UTKInputField();
password.label = ""비밀번호"";
password.isPasswordField = true;
-// 검증 오류 표시
-input.ErrorMessage = ""이름은 필수입니다."";
-input.ErrorMessage = """"; // 오류 제거
-
// 변형 스타일
input.Variant = UTKInputField.InputFieldVariant.Outlined;
@@ -39,13 +74,34 @@ input.OnFocused += () => Debug.Log(""포커스"");
input.OnBlurred += () => Debug.Log(""포커스 해제"");
input.OnSubmit += (value) => Debug.Log($""제출: {value}"");
-// 비활성화
+// ── Validation (입력 검증) ──────────────────────
+// 1) ErrorMessage + Validation 함수 설정
+var emailInput = new UTKInputField(""이메일"", ""example@email.com"");
+emailInput.ErrorMessage = ""올바른 이메일 형식이 아닙니다."";
+emailInput.Validation = () => emailInput.Value.Contains(""@"");
+// → Submit(Enter) 또는 FocusOut 시 자동 검증
+// → 실패: 붉은 외곽선 + 에러 메시지 표시
+// → 통과: 에러 상태 자동 해제
+
+// 2) 필수 입력 검증
+var nameInput = new UTKInputField(""이름"");
+nameInput.ErrorMessage = ""이름은 필수 항목입니다."";
+nameInput.Validation = () => !string.IsNullOrWhiteSpace(nameInput.Value);
+
+// 3) 강제 검증 호출 (폼 제출 버튼 등에서 사용)
+bool isValid = nameInput.Validate(); // Validation 실행 + UI 업데이트
+if (!isValid) return;
+
+// 4) 에러 수동 해제
+nameInput.ClearError();
+
+// 5) 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+input.ErrorMessage = ""서버 오류가 발생했습니다."";
+input.ErrorMessage = """"; // 오류 제거
+
+// 비활성화 / 읽기 전용
input.IsEnabled = false;
-
-// 읽기 전용
input.isReadOnly = true;
-
-// 여러 줄 입력
input.multiline = true;",
uxmlCode: @"
@@ -62,6 +118,9 @@ input.multiline = true;",
+
+
+
@@ -70,21 +129,78 @@ input.multiline = true;",
private void InitializeIntegerFieldSample(VisualElement root)
{
+ // Validation - Range (1~150)
+ var rangeField = root.Q("int-validation-range");
+ if (rangeField != null)
+ {
+ rangeField.ErrorMessage = "나이는 1~150 사이여야 합니다.";
+ rangeField.Validation = () => rangeField.Value >= 1 && rangeField.Value <= 150;
+ }
+
+ // Validation - Positive Only
+ var positiveField = root.Q("int-validation-positive");
+ if (positiveField != null)
+ {
+ positiveField.ErrorMessage = "수량은 0보다 커야 합니다.";
+ positiveField.Validation = () => positiveField.Value > 0;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("int-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var rangeValid = rangeField?.Validate() ?? true;
+ var positiveValid = positiveField?.Validate() ?? true;
+ var allValid = rangeValid && positiveValid;
+ Debug.Log($"IntegerField Validation: {(allValid ? "All Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ rangeField?.ClearError();
+ positiveField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 정수 필드
var intField = new UTKIntegerField(""나이"");
intField.Value = 25;
intField.OnValueChanged += (value) => Debug.Log($""나이: {value}"");
-// 범위 제한 (이벤트로 처리)
-intField.OnValueChanged += (value) =>
-{
- if (value < 0) intField.Value = 0;
- if (value > 150) intField.Value = 150;
-};
-
// 비활성화
-intField.IsEnabled = false;",
+intField.IsEnabled = false;
+
+// ── Validation (입력 검증) ──────────────────────
+// 1) ErrorMessage + Validation 함수 설정
+var ageField = new UTKIntegerField(""나이"", 0);
+ageField.ErrorMessage = ""나이는 1~150 사이여야 합니다."";
+ageField.Validation = () => ageField.Value >= 1 && ageField.Value <= 150;
+// → FocusOut 또는 Enter 시 자동 검증
+// → 실패: 붉은 외곽선 + 에러 메시지 표시
+// → 통과: 에러 상태 자동 해제
+
+// 2) 양수만 허용
+var qtyField = new UTKIntegerField(""수량"", 0);
+qtyField.ErrorMessage = ""수량은 0보다 커야 합니다."";
+qtyField.Validation = () => qtyField.Value > 0;
+
+// 3) 강제 검증 호출 (폼 제출 버튼 등에서 사용)
+bool isValid = ageField.Validate(); // Validation 실행 + UI 업데이트
+if (!isValid) return;
+
+// 4) 에러 수동 해제
+ageField.ClearError();
+
+// 5) 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+intField.ErrorMessage = ""서버 오류가 발생했습니다."";
+intField.ErrorMessage = """"; // 오류 제거",
uxmlCode: @"
@@ -102,6 +218,45 @@ intField.IsEnabled = false;",
private void InitializeLongFieldSample(VisualElement root)
{
+ // Validation - Range (0 ~ 1,000,000)
+ var rangeField = root.Q("long-validation-range");
+ if (rangeField != null)
+ {
+ rangeField.ErrorMessage = "파일 크기는 0 ~ 1,000,000 사이여야 합니다.";
+ rangeField.Validation = () => rangeField.Value >= 0 && rangeField.Value <= 1000000;
+ }
+
+ // Validation - Positive Only
+ var positiveField = root.Q("long-validation-positive");
+ if (positiveField != null)
+ {
+ positiveField.ErrorMessage = "개수는 0보다 커야 합니다.";
+ positiveField.Validation = () => positiveField.Value > 0;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("long-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var rangeValid = rangeField?.Validate() ?? true;
+ var positiveValid = positiveField?.Validate() ?? true;
+ var allValid = rangeValid && positiveValid;
+ Debug.Log($"LongField Validation: {(allValid ? "All Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ rangeField?.ClearError();
+ positiveField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 Long 필드
var longField = new UTKLongField(""파일 크기"");
@@ -109,7 +264,19 @@ longField.Value = 1073741824; // 1GB
longField.OnValueChanged += (value) => Debug.Log($""크기: {value} bytes"");
// 비활성화
-longField.IsEnabled = false;",
+longField.IsEnabled = false;
+
+// ── Validation (입력 검증) ──────────────────────
+// 1) ErrorMessage + Validation 함수 설정
+var sizeField = new UTKLongField(""파일 크기"", 0);
+sizeField.ErrorMessage = ""파일 크기는 0 ~ 1,000,000 사이여야 합니다."";
+sizeField.Validation = () => sizeField.Value >= 0 && sizeField.Value <= 1000000;
+
+// 2) 강제 검증 호출
+bool isValid = sizeField.Validate();
+
+// 3) 에러 수동 해제
+sizeField.ClearError();",
uxmlCode: @"
@@ -127,21 +294,66 @@ longField.IsEnabled = false;",
private void InitializeFloatFieldSample(VisualElement root)
{
+ // Validation - Range (0 ~ 100)
+ var rangeField = root.Q("float-validation-range");
+ if (rangeField != null)
+ {
+ rangeField.ErrorMessage = "속도는 0 ~ 100 사이여야 합니다.";
+ rangeField.Validation = () => rangeField.Value >= 0f && rangeField.Value <= 100f;
+ }
+
+ // Validation - Positive Only
+ var positiveField = root.Q("float-validation-positive");
+ if (positiveField != null)
+ {
+ positiveField.ErrorMessage = "무게는 0보다 커야 합니다.";
+ positiveField.Validation = () => positiveField.Value > 0f;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("float-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var rangeValid = rangeField?.Validate() ?? true;
+ var positiveValid = positiveField?.Validate() ?? true;
+ var allValid = rangeValid && positiveValid;
+ Debug.Log($"FloatField Validation: {(allValid ? "All Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ rangeField?.ClearError();
+ positiveField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 Float 필드
var floatField = new UTKFloatField(""속도"");
floatField.Value = 9.8f;
floatField.OnValueChanged += (value) => Debug.Log($""속도: {value}"");
-// 범위 제한 (이벤트로 처리)
-floatField.OnValueChanged += (value) =>
-{
- if (value < 0f) floatField.Value = 0f;
- if (value > 100f) floatField.Value = 100f;
-};
-
// 비활성화
-floatField.IsEnabled = false;",
+floatField.IsEnabled = false;
+
+// ── Validation (입력 검증) ──────────────────────
+// 1) ErrorMessage + Validation 함수 설정
+var speedField = new UTKFloatField(""속도"", 0f);
+speedField.ErrorMessage = ""속도는 0 ~ 100 사이여야 합니다."";
+speedField.Validation = () => speedField.Value >= 0f && speedField.Value <= 100f;
+// → FocusOut 또는 Enter 시 자동 검증
+
+// 2) 강제 검증 호출
+bool isValid = speedField.Validate();
+
+// 3) 에러 수동 해제
+speedField.ClearError();",
uxmlCode: @"
@@ -159,6 +371,45 @@ floatField.IsEnabled = false;",
private void InitializeDoubleFieldSample(VisualElement root)
{
+ // Validation - Range (0.0 ~ 100.0)
+ var rangeField = root.Q("double-validation-range");
+ if (rangeField != null)
+ {
+ rangeField.ErrorMessage = "값은 0.0 ~ 100.0 사이여야 합니다.";
+ rangeField.Validation = () => rangeField.Value >= 0.0 && rangeField.Value <= 100.0;
+ }
+
+ // Validation - Positive Only
+ var positiveField = root.Q("double-validation-positive");
+ if (positiveField != null)
+ {
+ positiveField.ErrorMessage = "거리는 0보다 커야 합니다.";
+ positiveField.Validation = () => positiveField.Value > 0.0;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("double-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var rangeValid = rangeField?.Validate() ?? true;
+ var positiveValid = positiveField?.Validate() ?? true;
+ var allValid = rangeValid && positiveValid;
+ Debug.Log($"DoubleField Validation: {(allValid ? "All Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ rangeField?.ClearError();
+ positiveField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 Double 필드
var doubleField = new UTKDoubleField(""정밀도"");
@@ -166,7 +417,20 @@ doubleField.Value = 3.141592653589793;
doubleField.OnValueChanged += (value) => Debug.Log($""값: {value}"");
// 비활성화
-doubleField.IsEnabled = false;",
+doubleField.IsEnabled = false;
+
+// ── Validation (입력 검증) ──────────────────────
+// 1) ErrorMessage + Validation 함수 설정
+var percentField = new UTKDoubleField(""퍼센트"", 0);
+percentField.ErrorMessage = ""값은 0.0 ~ 100.0 사이여야 합니다."";
+percentField.Validation = () => percentField.Value >= 0.0 && percentField.Value <= 100.0;
+// → FocusOut 또는 Enter 시 자동 검증
+
+// 2) 강제 검증 호출
+bool isValid = percentField.Validate();
+
+// 3) 에러 수동 해제
+percentField.ClearError();",
uxmlCode: @"
@@ -193,6 +457,39 @@ doubleField.IsEnabled = false;",
disabledRow.Add(stepper);
}
+ // Validation - Even Number Only
+ UTKIntStepper? validationStepper = null;
+ var validationRow = root.Q("stepper-validation-row");
+ if (validationRow != null)
+ {
+ validationStepper = new UTKIntStepper(0, 100, 1, 1);
+ validationStepper.style.width = 150;
+ validationStepper.ErrorMessage = "짝수만 입력 가능합니다.";
+ validationStepper.Validation = () => validationStepper.Value % 2 == 0;
+ validationRow.Add(validationStepper);
+ validationStepper.Validate();
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("stepper-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationStepper?.Validate() ?? true;
+ Debug.Log($"IntStepper Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationStepper?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 사용법
var stepper = new UTKIntStepper(minValue: 0, maxValue: 100, value: 50, step: 1);
@@ -213,7 +510,15 @@ disabledStepper.SetEnabled(false);
stepper.Value = 75;
stepper.MinValue = 10;
stepper.MaxValue = 90;
-stepper.Step = 2;",
+stepper.Step = 2;
+
+// ── Validation (입력 검증) ──────────────────────
+var evenStepper = new UTKIntStepper(0, 100, 0, 1);
+evenStepper.ErrorMessage = ""짝수만 입력 가능합니다."";
+evenStepper.Validation = () => evenStepper.Value % 2 == 0;
+
+bool isValid = evenStepper.Validate();
+evenStepper.ClearError();",
uxmlCode: @"
@@ -232,6 +537,105 @@ stepper.Step = 2;",
+");
+ }
+
+ private void InitializeFloatStepperSample(VisualElement root)
+ {
+ var disabledRow = root.Q("float-stepper-disabled-row");
+ if (disabledRow != null)
+ {
+ var stepper = new UTKFloatStepper(0f, 10f, 3.5f, 0.1f);
+ stepper.style.width = 150;
+ stepper.SetEnabled(false);
+ disabledRow.Add(stepper);
+ }
+
+ // Validation - Non-Zero
+ UTKFloatStepper? validationStepper = null;
+ var validationRow = root.Q("float-stepper-validation-row");
+ if (validationRow != null)
+ {
+ validationStepper = new UTKFloatStepper(0f, 10f, 0f, 0.1f);
+ validationStepper.style.width = 150;
+ validationStepper.ErrorMessage = "값은 0이 될 수 없습니다.";
+ validationStepper.Validation = () => validationStepper.Value != 0f;
+ validationRow.Add(validationStepper);
+ validationStepper.Validate();
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("float-stepper-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationStepper?.Validate() ?? true;
+ Debug.Log($"FloatStepper Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationStepper?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
+ SetCodeSamples(root,
+ csharpCode: @"// 기본 사용법
+var stepper = new UTKFloatStepper(minValue: 0f, maxValue: 10f, initialValue: 5.5f, step: 0.1f);
+stepper.OnValueChanged += (newValue) => Debug.Log($""Value: {newValue}"");
+
+// Step 설정
+var stepper05 = new UTKFloatStepper(0f, 10f, 2.5f, 0.5f); // Step 0.5씩 증감
+
+// Wrap Around (최소/최대값에서 순환)
+var wrapStepper = new UTKFloatStepper(0f, 1f, 0.5f, 0.1f);
+wrapStepper.WrapAround = true; // 0.0 -> -0.1 시 1.0으로 순환
+
+// 비활성화
+var disabledStepper = new UTKFloatStepper(0f, 10f, 3.5f, 0.1f);
+disabledStepper.SetEnabled(false);
+
+// 속성 변경
+stepper.Value = 7.5f;
+stepper.MinValue = 1f;
+stepper.MaxValue = 9f;
+stepper.Step = 0.25f;
+
+// 프로그래밍 방식으로 값 변경
+stepper.Increment(); // Step만큼 증가
+stepper.Decrement(); // Step만큼 감소
+stepper.SetValue(4.2f); // 직접 설정
+
+// ── Validation (입력 검증) ──────────────────────
+var nonZeroStepper = new UTKFloatStepper(0f, 10f, 0f, 0.1f);
+nonZeroStepper.ErrorMessage = ""값은 0이 될 수 없습니다."";
+nonZeroStepper.Validation = () => nonZeroStepper.Value != 0f;
+
+bool isValid = nonZeroStepper.Validate();
+nonZeroStepper.ClearError();",
+ uxmlCode: @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
");
}
@@ -243,6 +647,34 @@ stepper.Step = 2;",
field.Value = new Vector2(100, 200);
}
+ // Validation - Non-Zero
+ var validationField = root.Q("vec2-validation");
+ if (validationField != null)
+ {
+ validationField.ErrorMessage = "방향 벡터는 (0,0)이 될 수 없습니다.";
+ validationField.Validation = () => validationField.Value != Vector2.zero;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("vec2-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationField?.Validate() ?? true;
+ Debug.Log($"Vector2Field Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 사용법
var positionField = new UTKVector2Field(""Position"");
@@ -255,7 +687,15 @@ field.label = """";
// 읽기 전용
var readOnlyField = new UTKVector2Field(""ReadOnly"");
-readOnlyField.SetEnabled(false);",
+readOnlyField.SetEnabled(false);
+
+// ── Validation (입력 검증) ──────────────────────
+var dirField = new UTKVector2Field(""Direction"");
+dirField.ErrorMessage = ""방향 벡터는 (0,0)이 될 수 없습니다."";
+dirField.Validation = () => dirField.Value != Vector2.zero;
+
+bool isValid = dirField.Validate();
+dirField.ClearError();",
uxmlCode: @"
@@ -279,6 +719,34 @@ readOnlyField.SetEnabled(false);",
field.Value = new Vector3(10, 20, 30);
}
+ // Validation - Non-Zero
+ var validationField = root.Q("vec3-validation");
+ if (validationField != null)
+ {
+ validationField.ErrorMessage = "스케일 벡터는 (0,0,0)이 될 수 없습니다.";
+ validationField.Validation = () => validationField.Value != Vector3.zero;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("vec3-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationField?.Validate() ?? true;
+ Debug.Log($"Vector3Field Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 사용법
var positionField = new UTKVector3Field(""Position"");
@@ -291,7 +759,15 @@ rotationField.Value = new Vector3(0, 90, 0);
// 비활성화
var disabledField = new UTKVector3Field(""Disabled"");
-disabledField.SetEnabled(false);",
+disabledField.SetEnabled(false);
+
+// ── Validation (입력 검증) ──────────────────────
+var scaleField = new UTKVector3Field(""Scale"");
+scaleField.ErrorMessage = ""스케일 벡터는 (0,0,0)이 될 수 없습니다."";
+scaleField.Validation = () => scaleField.Value != Vector3.zero;
+
+bool isValid = scaleField.Validate();
+scaleField.ClearError();",
uxmlCode: @"
@@ -315,6 +791,42 @@ disabledField.SetEnabled(false);",
field.Value = new Vector4(1, 0.5f, 0.25f, 1);
}
+ // Validation - 0~1 Range (RGBA)
+ var validationField = root.Q("vec4-validation");
+ if (validationField != null)
+ {
+ validationField.Value = new Vector4(1, 0.5f, 0.25f, 1);
+ validationField.ErrorMessage = "RGBA 값은 모두 0~1 사이여야 합니다.";
+ validationField.Validation = () =>
+ {
+ var v = validationField.Value;
+ return v.x >= 0f && v.x <= 1f &&
+ v.y >= 0f && v.y <= 1f &&
+ v.z >= 0f && v.z <= 1f &&
+ v.w >= 0f && v.w <= 1f;
+ };
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("vec4-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationField?.Validate() ?? true;
+ Debug.Log($"Vector4Field Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 사용법
var colorField = new UTKVector4Field(""Color"");
@@ -328,7 +840,20 @@ quaternionField.Value = new Vector4(quat.x, quat.y, quat.z, quat.w);
// 비활성화
var disabledField = new UTKVector4Field(""Disabled"");
-disabledField.SetEnabled(false);",
+disabledField.SetEnabled(false);
+
+// ── Validation (입력 검증) ──────────────────────
+var rgbaField = new UTKVector4Field(""RGBA"");
+rgbaField.ErrorMessage = ""RGBA 값은 모두 0~1 사이여야 합니다."";
+rgbaField.Validation = () =>
+{
+ var v = rgbaField.Value;
+ return v.x >= 0f && v.x <= 1f && v.y >= 0f && v.y <= 1f
+ && v.z >= 0f && v.z <= 1f && v.w >= 0f && v.w <= 1f;
+};
+
+bool isValid = rgbaField.Validate();
+rgbaField.ClearError();",
uxmlCode: @"
@@ -352,6 +877,35 @@ disabledField.SetEnabled(false);",
field.Value = new Rect(10, 20, 100, 50);
}
+ // Validation - Positive Size
+ var validationField = root.Q("rect-validation");
+ if (validationField != null)
+ {
+ validationField.ErrorMessage = "Width와 Height는 0보다 커야 합니다.";
+ validationField.Validation = () =>
+ validationField.Value.width > 0 && validationField.Value.height > 0;
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("rect-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationField?.Validate() ?? true;
+ Debug.Log($"RectField Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 사용법
var areaField = new UTKRectField(""Area"");
@@ -364,7 +918,16 @@ screenArea.Value = new Rect(0, 0, Screen.width, Screen.height);
// 비활성화
var disabledField = new UTKRectField(""Disabled"");
-disabledField.SetEnabled(false);",
+disabledField.SetEnabled(false);
+
+// ── Validation (입력 검증) ──────────────────────
+var viewportField = new UTKRectField(""Viewport"");
+viewportField.ErrorMessage = ""Width와 Height는 0보다 커야 합니다."";
+viewportField.Validation = () =>
+ viewportField.Value.width > 0 && viewportField.Value.height > 0;
+
+bool isValid = viewportField.Validate();
+viewportField.ClearError();",
uxmlCode: @"
@@ -388,6 +951,38 @@ disabledField.SetEnabled(false);",
field.Value = new Bounds(new Vector3(0, 1, 0), new Vector3(2, 2, 2));
}
+ // Validation - Positive Extents
+ var validationField = root.Q("bounds-validation");
+ if (validationField != null)
+ {
+ validationField.ErrorMessage = "Extents는 모두 0보다 커야 합니다.";
+ validationField.Validation = () =>
+ {
+ var ext = validationField.Value.extents;
+ return ext.x > 0f && ext.y > 0f && ext.z > 0f;
+ };
+ }
+
+ // Validate All / Clear Errors 버튼
+ var buttonRow = root.Q("bounds-validation-button-row");
+ if (buttonRow != null)
+ {
+ var validateBtn = new UTKButton("Validate All", variant: UTKButton.ButtonVariant.Primary);
+ validateBtn.OnClicked += () =>
+ {
+ var isValid = validationField?.Validate() ?? true;
+ Debug.Log($"BoundsField Validation: {(isValid ? "Passed" : "Has Errors")}");
+ };
+ buttonRow.Add(validateBtn);
+
+ var clearBtn = new UTKButton("Clear Errors");
+ clearBtn.OnClicked += () =>
+ {
+ validationField?.ClearError();
+ };
+ buttonRow.Add(clearBtn);
+ }
+
SetCodeSamples(root,
csharpCode: @"// 기본 사용법
var boundsField = new UTKBoundsField(""Bounds"");
@@ -402,7 +997,19 @@ collisionField.Value = new Bounds(Vector3.zero, Vector3.one * 5);
// 비활성화
var disabledField = new UTKBoundsField(""Disabled"");
-disabledField.SetEnabled(false);",
+disabledField.SetEnabled(false);
+
+// ── Validation (입력 검증) ──────────────────────
+var colliderField = new UTKBoundsField(""Collider"");
+colliderField.ErrorMessage = ""Extents는 모두 0보다 커야 합니다."";
+colliderField.Validation = () =>
+{
+ var ext = colliderField.Value.extents;
+ return ext.x > 0f && ext.y > 0f && ext.z > 0f;
+};
+
+bool isValid = colliderField.Validate();
+colliderField.ClearError();",
uxmlCode: @"
diff --git a/Assets/Sample/UIToolkit/UTKStyleGuideSample.cs b/Assets/Sample/UIToolkit/UTKStyleGuideSample.cs
index 9f27350b..f928db5a 100644
--- a/Assets/Sample/UIToolkit/UTKStyleGuideSample.cs
+++ b/Assets/Sample/UIToolkit/UTKStyleGuideSample.cs
@@ -68,6 +68,7 @@ public partial class UTKStyleGuideSample : MonoBehaviour
["UTKRectField"] = "UIToolkit/Sample/Input/UTKRectFieldSample",
["UTKBoundsField"] = "UIToolkit/Sample/Input/UTKBoundsFieldSample",
["UTKIntStepper"] = "UIToolkit/Sample/Input/UTKIntStepperSample",
+ ["UTKFloatStepper"] = "UIToolkit/Sample/Input/UTKFloatStepperSample",
// Slider
["UTKSlider"] = "UIToolkit/Sample/Slider/UTKSliderSample",
["UTKSliderInt"] = "UIToolkit/Sample/Slider/UTKSliderIntSample",
@@ -111,7 +112,7 @@ public partial class UTKStyleGuideSample : MonoBehaviour
{
["Icon"] = new[] { "UTKMaterialIcons", "UTKImageIcons", "UTKImage" },
["Button"] = new[] { "UTKButton", "UTKCheckBox", "UTKToggle", "UTKRadioButton", "UTKToggleButtonGroup" },
- ["Input"] = new[] { "UTKInputField", "UTKIntegerField", "UTKLongField", "UTKFloatField", "UTKDoubleField", "UTKVector2Field", "UTKVector3Field", "UTKVector4Field", "UTKRectField", "UTKBoundsField", "UTKIntStepper" },
+ ["Input"] = new[] { "UTKInputField", "UTKIntegerField", "UTKLongField", "UTKFloatField", "UTKDoubleField", "UTKVector2Field", "UTKVector3Field", "UTKVector4Field", "UTKRectField", "UTKBoundsField", "UTKIntStepper", "UTKFloatStepper" },
["Slider"] = new[] { "UTKSlider", "UTKSliderInt", "UTKMinMaxSlider", "UTKProgressBar" },
["Dropdown"] = new[] { "UTKDropdown", "UTKEnumDropDown", "UTKMultiSelectDropdown" },
["Label"] = new[] { "UTKLabel", "UTKHelpBox" },
@@ -451,6 +452,9 @@ public partial class UTKStyleGuideSample : MonoBehaviour
case "UTKIntStepper":
InitializeNumberStepperSample(root);
break;
+ case "UTKFloatStepper":
+ InitializeFloatStepperSample(root);
+ break;
case "UTKVector2Field":
InitializeVector2FieldSample(root);
break;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKBoundsField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKBoundsField.cs
index 9141c05f..e198e62a 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKBoundsField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKBoundsField.cs
@@ -16,6 +16,10 @@ namespace UVC.UIToolkit
/// - Center: 경계 상자의 중심점 (Vector3)
/// - Extents: 중심에서 각 축 방향으로의 거리 (Vector3), Size의 절반 값
/// - Size: 경계 상자의 전체 크기 (Extents * 2)
+ ///
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
+ ///
///
///
/// C# 코드에서 사용:
@@ -52,6 +56,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Bounds(Vector3.zero, Vector3.one);
/// readOnlyField.IsReadOnly = true;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var boundsField = new UTKBoundsField("경계");
+ /// boundsField.ErrorMessage = "크기는 양수여야 합니다.";
+ /// boundsField.Validation = () => boundsField.Value.size.x > 0 && boundsField.Value.size.y > 0 && boundsField.Value.size.z > 0;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = boundsField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// boundsField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// boundsField.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// boundsField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
///
@@ -70,6 +94,9 @@ namespace UVC.UIToolkit
///
///
///
+ ///
+ ///
+ ///
///
///
/// 실제 활용 예시:
@@ -102,6 +129,9 @@ namespace UVC.UIToolkit
private string _xLabel = "X";
private string _yLabel = "Y";
private string _zLabel = "Z";
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -202,6 +232,27 @@ namespace UVC.UIToolkit
EnableInClassList("utk-boundsfield--readonly", value);
}
}
+
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-boundsfield--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
#endregion
#region Constructor
@@ -238,6 +289,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -303,6 +355,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-boundsfield--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-boundsfield--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-boundsfield__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -314,8 +433,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs
index 00b82d70..b953c89c 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs
@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// - 과학 계산, 금융 데이터, 정밀 측정값에 사용
/// - float보다 약 2배 정밀도 (유효숫자 15-17자리)
/// - 일반 게임 로직에는 UTKFloatField로 충분
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
///
/// float vs double:
///
@@ -44,6 +46,20 @@ namespace UVC.UIToolkit
/// double currentValue = doubleField.Value;
/// doubleField.Value = 127.9780;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var precisionField = new UTKDoubleField("정밀 값", 0);
+ /// precisionField.ErrorMessage = "값은 0보다 커야 합니다.";
+ /// precisionField.Validation = () => precisionField.Value > 0;
+ /// // → FocusOut 시 자동으로 검증
+ ///
+ /// // 강제 검증 호출
+ /// bool isValid = precisionField.Validate();
+ ///
+ /// // 에러 수동 해제
+ /// precisionField.ClearError();
+ ///
/// UXML에서 사용:
///
@@ -72,6 +88,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -87,6 +106,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-double-field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+
/// 활성화 상태
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -133,6 +173,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
+ RegisterCallback(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -165,6 +207,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+
+ private void OnKeyDown(KeyDownEvent evt)
+ {
+ if (evt.keyCode == KeyCode.Return)
+ {
+ RunValidation();
+ }
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-double-field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-double-field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-double-field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -176,8 +293,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ UnregisterCallback(OnKeyDown);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs
index 31d9860f..d64d8f67 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs
@@ -18,6 +18,8 @@ namespace UVC.UIToolkit
/// - 가격, 속도, 거리 등 실수 값에 적합
/// - 더 높은 정밀도가 필요하면 UTKDoubleField 사용
/// - 정수만 필요하면 UTKIntegerField 사용
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
///
/// 주의: float 리터럴은 숫자 뒤에 'f'를 붙여야 합니다 (예: 3.14f)
///
@@ -44,6 +46,31 @@ namespace UVC.UIToolkit
/// // 비활성화
/// floatField.IsEnabled = false;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var speedField = new UTKFloatField("속도", 0f);
+ /// speedField.ErrorMessage = "속도는 0보다 커야 합니다.";
+ /// speedField.Validation = () => speedField.Value > 0f;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 범위 검증
+ /// var temperatureField = new UTKFloatField("온도", 20f);
+ /// temperatureField.ErrorMessage = "온도는 -40 ~ 60 사이여야 합니다.";
+ /// temperatureField.Validation = () => temperatureField.Value >= -40f && temperatureField.Value <= 60f;
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = speedField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// speedField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// floatField.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// floatField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
@@ -56,6 +83,8 @@ namespace UVC.UIToolkit
///
/// // 캐릭터 이동 속도 편집
/// var moveSpeedField = new UTKFloatField("이동 속도", character.MoveSpeed);
+ /// moveSpeedField.ErrorMessage = "이동 속도는 0 이상이어야 합니다.";
+ /// moveSpeedField.Validation = () => moveSpeedField.Value >= 0f;
/// moveSpeedField.OnValueChanged += (newSpeed) => {
/// character.MoveSpeed = Mathf.Clamp(newSpeed, 0f, 100f);
/// };
@@ -71,6 +100,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -86,6 +118,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-float-field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+
/// 활성화 상태
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -132,6 +185,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
+ RegisterCallback(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -164,6 +219,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+
+ private void OnKeyDown(KeyDownEvent evt)
+ {
+ if (evt.keyCode == KeyCode.Return)
+ {
+ RunValidation();
+ }
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-float-field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-float-field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-float-field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -175,8 +305,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ UnregisterCallback(OnKeyDown);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs
index 31404332..07a775c9 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs
@@ -25,6 +25,12 @@ namespace UVC.UIToolkit
/// - 증감 단위 설정 (Step)
/// - 순환 모드 (WrapAround) - 최대에서 최소로, 최소에서 최대로
///
+ /// Validation (유효성 검사):
+ ///
+ /// 속성에 검증 함수를 설정하면,
+ /// 포커스 아웃 시 자동으로 유효성 검사를 수행합니다.
+ /// 검증 실패 시 에 설정된 메시지가 표시됩니다.
+ ///
///
///
/// C# 코드에서 사용:
@@ -56,6 +62,12 @@ namespace UVC.UIToolkit
/// var readOnlyStepper = new UTKFloatStepper(0f, 10f, 5f, 0.1f);
/// readOnlyStepper.IsReadOnly = true;
///
+ /// Validation 사용:
+ ///
+ /// var volumeStepper = new UTKFloatStepper(0f, 1f, 0.5f, 0.1f);
+ /// volumeStepper.ErrorMessage = "볼륨은 0~1 사이여야 합니다.";
+ /// volumeStepper.Validation = () => volumeStepper.Value >= 0f && volumeStepper.Value <= 1f;
+ ///
/// UXML에서 사용:
///
@@ -69,6 +81,9 @@ namespace UVC.UIToolkit
///
///
///
+ ///
+ ///
+ ///
/// ]]>
/// 실제 활용 예시 (볼륨 조절):
///
@@ -142,6 +157,20 @@ namespace UVC.UIToolkit
EnableInClassList("utk-number-stepper--readonly", value);
}
}
+
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-number-stepper--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
#endregion
#region Fields
@@ -154,10 +183,29 @@ namespace UVC.UIToolkit
private bool _wrapAround;
private bool _isUpdating;
private bool _isHovered;
+ private string _errorMessage = "";
private TextField? _textField;
private Button? _upButton;
private Button? _downButton;
+
+ /// 유효성 검사 함수. true 반환 시 유효, false 반환 시 에러 표시
+ private Func? _validation;
+
+ /// 에러 메시지 표시용 레이블
+ private Label? _errorLabel;
+ #endregion
+
+ #region Properties
+ ///
+ /// 유효성 검사 함수.
+ /// 포커스 아웃 시 자동으로 호출되며, false 반환 시 에러 스타일과 메시지가 표시됩니다.
+ ///
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
#endregion
#region Events
@@ -241,6 +289,7 @@ namespace UVC.UIToolkit
if (notify)
{
OnValueChanged?.Invoke(_value);
+ RunValidation();
}
}
}
@@ -288,6 +337,18 @@ namespace UVC.UIToolkit
{
_textField?.Focus();
}
+
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
#endregion
#region Private Methods - UI Creation
@@ -344,6 +405,7 @@ namespace UVC.UIToolkit
_textField?.RegisterCallback>(OnTextFieldChanged);
_textField?.RegisterCallback(OnTextFieldKeyDown, TrickleDown.TrickleDown);
+ _textField?.RegisterCallback(OnTextFieldFocusOut);
RegisterCallback(OnMouseEnter);
RegisterCallback(OnMouseLeave);
@@ -397,6 +459,12 @@ namespace UVC.UIToolkit
}
}
+ /// 텍스트 필드 포커스 아웃 시 유효성 검사 실행
+ private void OnTextFieldFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+
private void OnMouseEnter(MouseEnterEvent evt) => _isHovered = true;
private void OnMouseLeave(MouseLeaveEvent evt) => _isHovered = false;
@@ -433,9 +501,25 @@ namespace UVC.UIToolkit
private float ClampValueInternal(float value)
{
+ // Step 소수점 자릿수 기준으로 반올림하여 부동소수점 오차 제거
+ // 예: step=0.1 → digits=1, 0.7+0.1=0.8000001 → 0.8
+ int digits = GetDecimalDigits(_step);
+ if (digits > 0)
+ {
+ value = (float)Math.Round(value, digits);
+ }
return Mathf.Clamp(value, _minValue, _maxValue);
}
+ /// 소수점 이하 유효 자릿수를 반환합니다.
+ private static int GetDecimalDigits(float value)
+ {
+ // 최대 7자리까지 검사 (float 정밀도 한계)
+ string s = value.ToString("G7");
+ int dotIndex = s.IndexOf('.');
+ return dotIndex < 0 ? 0 : s.Length - dotIndex - 1;
+ }
+
private void UpdateReadOnlyState()
{
if (_textField != null)
@@ -453,6 +537,47 @@ namespace UVC.UIToolkit
_downButton.SetEnabled(!_isReadOnly);
}
}
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ EnableInClassList("utk-number-stepper--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ EnableInClassList("utk-number-stepper--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-number-stepper__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -471,6 +596,7 @@ namespace UVC.UIToolkit
_textField?.UnregisterCallback>(OnTextFieldChanged);
_textField?.UnregisterCallback(OnTextFieldKeyDown, TrickleDown.TrickleDown);
+ _textField?.UnregisterCallback(OnTextFieldFocusOut);
UnregisterCallback(OnMouseEnter);
UnregisterCallback(OnMouseLeave);
@@ -481,6 +607,10 @@ namespace UVC.UIToolkit
OnTabPressed = null;
OnShiftTabPressed = null;
+ // Validation 정리
+ _validation = null;
+ _errorLabel = null;
+
// UI 참조 정리
_textField = null;
_upButton = null;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKInputField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKInputField.cs
index e650a9cd..1e24e084 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKInputField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKInputField.cs
@@ -9,6 +9,15 @@ namespace UVC.UIToolkit
/// 입력 필드 컴포넌트.
/// Unity TextField를 래핑하여 커스텀 스타일을 적용합니다.
///
+ ///
+ /// 주요 기능:
+ ///
+ /// - 플레이스홀더, 비밀번호, 멀티라인 지원
+ /// - 스타일 변형 (Default, Filled, Outlined)
+ /// - Validation 함수를 통한 입력 검증 (Submit/FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
+ ///
+ ///
///
/// C# 코드에서 사용:
///
@@ -17,39 +26,62 @@ namespace UVC.UIToolkit
/// input.label = "이름";
/// input.Placeholder = "이름을 입력하세요";
/// input.OnValueChanged += (value) => Debug.Log($"입력값: {value}");
- ///
+ ///
/// // 비밀번호 입력 필드
/// var password = new UTKInputField();
/// password.label = "비밀번호";
/// password.isPasswordField = true;
- ///
- /// // 검증 오류 표시
- /// input.ErrorMessage = "이름은 필수입니다.";
- /// // 오류 제거
- /// input.ErrorMessage = "";
- ///
+ ///
/// // 변형 스타일
/// input.Variant = UTKInputField.InputFieldVariant.Outlined;
///
- /// UXML에서 사용:
+ /// Validation (입력 검증):
///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var emailInput = new UTKInputField("이메일", "example@email.com");
+ /// emailInput.ErrorMessage = "올바른 이메일 형식이 아닙니다.";
+ /// emailInput.Validation = () => emailInput.Value.Contains("@");
+ /// // → Submit(Enter) 또는 FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 필수 입력 검증
+ /// var nameInput = new UTKInputField("이름");
+ /// nameInput.ErrorMessage = "이름은 필수 항목입니다.";
+ /// nameInput.Validation = () => !string.IsNullOrWhiteSpace(nameInput.Value);
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = nameInput.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// nameInput.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이)
+ /// input.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// input.ErrorMessage = ""; // 오류 제거
+ ///
+ /// UXML에서 사용:
+ ///
///
///
- ///
+ ///
///
///
- ///
+ ///
///
///
- ///
+ ///
///
///
- ///
+ ///
+ ///
+ ///
+ ///
///
///
///
- ///
+ /// ]]>
///
[UxmlElement]
public partial class UTKInputField : TextField, IDisposable
@@ -63,6 +95,8 @@ namespace UVC.UIToolkit
private bool _isEnabled = true;
private string _errorMessage = "";
private InputFieldVariant _variant = InputFieldVariant.Default;
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -92,7 +126,7 @@ namespace UVC.UIToolkit
set => textEdition.placeholder = value;
}
- /// 에러 메시지
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
[UxmlAttribute("error-message")]
public string ErrorMessage
{
@@ -100,10 +134,19 @@ namespace UVC.UIToolkit
set
{
_errorMessage = value;
- EnableInClassList("utk-input--error", !string.IsNullOrEmpty(value));
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-input--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
}
}
+ /// 검증 함수. Submit/FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+
/// 활성화 상태
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -199,26 +242,9 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnTextValueChanged);
-
- RegisterCallback(_ =>
- {
- EnableInClassList("utk-input--focused", true);
- OnFocused?.Invoke();
- });
-
- RegisterCallback(_ =>
- {
- EnableInClassList("utk-input--focused", false);
- OnBlurred?.Invoke();
- });
-
- RegisterCallback(evt =>
- {
- if (evt.keyCode == KeyCode.Return && !multiline)
- {
- OnSubmit?.Invoke(value);
- }
- }, TrickleDown.TrickleDown);
+ RegisterCallback(OnFocusIn);
+ RegisterCallback(OnFocusOut);
+ RegisterCallback(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -263,6 +289,28 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusIn(FocusInEvent evt)
+ {
+ EnableInClassList("utk-input--focused", true);
+ OnFocused?.Invoke();
+ }
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ EnableInClassList("utk-input--focused", false);
+ OnBlurred?.Invoke();
+ RunValidation();
+ }
+
+ private void OnKeyDown(KeyDownEvent evt)
+ {
+ if (evt.keyCode == KeyCode.Return && !multiline)
+ {
+ OnSubmit?.Invoke(value);
+ RunValidation();
+ }
+ }
#endregion
#region Methods
@@ -283,6 +331,15 @@ namespace UVC.UIToolkit
}
}
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
///
/// 선택 영역 설정
///
@@ -291,6 +348,57 @@ namespace UVC.UIToolkit
textSelection.SelectAll();
}
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-input--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-input--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-input__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
+
private void UpdateVariant()
{
RemoveFromClassList("utk-input--default");
@@ -318,10 +426,16 @@ namespace UVC.UIToolkit
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
UnregisterCallback>(OnTextValueChanged);
+ UnregisterCallback(OnFocusIn);
+ UnregisterCallback(OnFocusOut);
+ UnregisterCallback(OnKeyDown);
+
OnValueChanged = null;
OnFocused = null;
OnBlurred = null;
OnSubmit = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs
index a4336555..0fe218d0 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs
@@ -24,6 +24,8 @@ namespace UVC.UIToolkit
/// - 최소/최대값 제한 (MinValue, MaxValue)
/// - 증감 단위 설정 (Step)
/// - 순환 모드 (WrapAround) - 최대에서 최소로, 최소에서 최대로
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
///
///
///
@@ -79,6 +81,15 @@ namespace UVC.UIToolkit
/// UpdateCalendar(month);
/// };
///
+ /// Validation (입력 검증):
+ ///
+ /// var monthStepper = new UTKIntStepper(1, 12, 1, 1);
+ /// monthStepper.ErrorMessage = "유효하지 않은 월입니다.";
+ /// monthStepper.Validation = () => monthStepper.Value >= 1 && monthStepper.Value <= 12;
+ ///
+ /// bool isValid = monthStepper.Validate();
+ /// monthStepper.ClearError();
+ ///
///
[UxmlElement]
public partial class UTKIntStepper : VisualElement, IDisposable
@@ -143,6 +154,20 @@ namespace UVC.UIToolkit
EnableInClassList("utk-number-stepper--readonly", value);
}
}
+
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-number-stepper--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
#endregion
#region Fields
@@ -156,11 +181,24 @@ namespace UVC.UIToolkit
private bool _isUpdating;
private bool _isHovered;
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
+
private TextField? _textField;
private Button? _upButton;
private Button? _downButton;
#endregion
+ #region Properties
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+ #endregion
+
#region Events
/// 값이 변경될 때 발생
public event Action? OnValueChanged;
@@ -242,6 +280,7 @@ namespace UVC.UIToolkit
if (notify)
{
OnValueChanged?.Invoke(_value);
+ RunValidation();
}
}
}
@@ -289,6 +328,18 @@ namespace UVC.UIToolkit
{
_textField?.Focus();
}
+
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
#endregion
#region Private Methods - UI Creation
@@ -345,6 +396,7 @@ namespace UVC.UIToolkit
_textField?.RegisterCallback>(OnTextFieldChanged);
_textField?.RegisterCallback(OnTextFieldKeyDown, TrickleDown.TrickleDown);
+ _textField?.RegisterCallback(OnTextFieldFocusOut);
RegisterCallback(OnMouseEnter);
RegisterCallback(OnMouseLeave);
@@ -371,6 +423,11 @@ namespace UVC.UIToolkit
}
}
+ private void OnTextFieldFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+
private void OnTextFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.UpArrow)
@@ -454,6 +511,47 @@ namespace UVC.UIToolkit
_downButton.SetEnabled(!_isReadOnly);
}
}
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ EnableInClassList("utk-number-stepper--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ EnableInClassList("utk-number-stepper--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-number-stepper__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -472,6 +570,7 @@ namespace UVC.UIToolkit
_textField?.UnregisterCallback>(OnTextFieldChanged);
_textField?.UnregisterCallback(OnTextFieldKeyDown, TrickleDown.TrickleDown);
+ _textField?.UnregisterCallback(OnTextFieldFocusOut);
UnregisterCallback(OnMouseEnter);
UnregisterCallback(OnMouseLeave);
@@ -483,6 +582,8 @@ namespace UVC.UIToolkit
OnShiftTabPressed = null;
// UI 참조 정리
+ _validation = null;
+ _errorLabel = null;
_textField = null;
_upButton = null;
_downButton = null;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs
index aa75396e..a76d7eca 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs
@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// - 개수, 수량, 인덱스 등 정수 값에 사용
/// - 큰 숫자가 필요하면 UTKLongField 사용
/// - 소수점이 필요하면 UTKFloatField 사용
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
///
///
///
@@ -42,6 +44,31 @@ namespace UVC.UIToolkit
/// // 비활성화
/// intField.IsEnabled = false;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var ageField = new UTKIntegerField("나이", 0);
+ /// ageField.ErrorMessage = "나이는 1~150 사이여야 합니다.";
+ /// ageField.Validation = () => ageField.Value >= 1 && ageField.Value <= 150;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 양수만 허용
+ /// var quantityField = new UTKIntegerField("수량", 0);
+ /// quantityField.ErrorMessage = "수량은 0보다 커야 합니다.";
+ /// quantityField.Validation = () => quantityField.Value > 0;
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = ageField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// ageField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// intField.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// intField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
@@ -54,8 +81,10 @@ namespace UVC.UIToolkit
///
/// // 인벤토리 아이템 수량 편집
/// var quantityField = new UTKIntegerField("보유 수량", item.Quantity);
+ /// quantityField.ErrorMessage = "수량은 0 이상이어야 합니다.";
+ /// quantityField.Validation = () => quantityField.Value >= 0;
/// quantityField.OnValueChanged += (newQty) => {
- /// item.Quantity = Mathf.Max(0, newQty); // 음수 방지
+ /// item.Quantity = newQty;
/// UpdateInventoryUI();
/// };
///
@@ -70,6 +99,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -85,6 +117,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-integer-field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+
/// 활성화 상태
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -131,6 +184,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
+ RegisterCallback(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -163,6 +218,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+
+ private void OnKeyDown(KeyDownEvent evt)
+ {
+ if (evt.keyCode == KeyCode.Return)
+ {
+ RunValidation();
+ }
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-integer-field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-integer-field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-integer-field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -174,8 +304,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ UnregisterCallback(OnKeyDown);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs
index 9d1d1562..0fcedfc3 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs
@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// - 파일 크기(바이트), 타임스탬프, 고유 ID 등에 사용
/// - int 범위(-21억~21억)를 초과하는 값에 적합
/// - 일반 정수는 UTKIntegerField 사용 권장 (메모리 효율)
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
///
///
///
@@ -39,6 +41,26 @@ namespace UVC.UIToolkit
/// long currentValue = longField.Value;
/// longField.Value = 5000000000L; // 50억
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var fileSizeField = new UTKLongField("파일 크기", 0);
+ /// fileSizeField.ErrorMessage = "파일 크기는 0보다 커야 합니다.";
+ /// fileSizeField.Validation = () => fileSizeField.Value > 0;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = fileSizeField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// fileSizeField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// longField.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// longField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
@@ -64,6 +86,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -79,6 +104,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-long-field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+
/// 활성화 상태
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -125,6 +171,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
+ RegisterCallback(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -157,6 +205,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+
+ private void OnKeyDown(KeyDownEvent evt)
+ {
+ if (evt.keyCode == KeyCode.Return)
+ {
+ RunValidation();
+ }
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-long-field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-long-field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-long-field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -168,8 +291,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ UnregisterCallback(OnKeyDown);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs
index cc9b8f62..952a1278 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs
@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// - X, Y: 사각형의 왼쪽 상단 모서리 위치
/// - Width: 사각형의 너비
/// - Height: 사각형의 높이
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
///
/// 주로 UI 요소의 위치/크기, 스프라이트 영역, 화면 좌표 등에 사용됩니다.
///
@@ -47,6 +49,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Rect(10, 10, 200, 100);
/// readOnlyField.IsReadOnly = true;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var areaField = new UTKRectField("영역");
+ /// areaField.ErrorMessage = "너비와 높이는 0보다 커야 합니다.";
+ /// areaField.Validation = () => areaField.Value.width > 0 && areaField.Value.height > 0;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = areaField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// areaField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// areaField.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// areaField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
@@ -68,6 +90,8 @@ namespace UVC.UIToolkit
/// // 스프라이트 UV 영역 편집기
/// var uvField = new UTKRectField("UV 영역");
/// uvField.Value = sprite.rect;
+ /// uvField.ErrorMessage = "UV 영역은 텍스처 범위를 초과할 수 없습니다.";
+ /// uvField.Validation = () => uvField.Value.xMax <= texture.width && uvField.Value.yMax <= texture.height;
/// uvField.OnValueChanged += (newRect) => {
/// // 스프라이트 영역 업데이트
/// UpdateSpriteRect(sprite, newRect);
@@ -89,6 +113,9 @@ namespace UVC.UIToolkit
private string _yLabel = "Y";
private string _wLabel = "W";
private string _hLabel = "H";
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -117,6 +144,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-rectfield--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
+
/// X 라벨
[UxmlAttribute("x-label")]
public string XLabel
@@ -221,6 +269,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -276,6 +325,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-rectfield--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-rectfield--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-rectfield__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -287,8 +403,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs
index a4433278..e66d5b75 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs
@@ -16,6 +16,13 @@ namespace UVC.UIToolkit
/// - X: 수평(가로) 방향의 값
/// - Y: 수직(세로) 방향의 값
/// 주로 2D 게임의 위치, UI 크기, 텍스처 좌표(UV) 등에 사용됩니다.
+ /// 주요 기능:
+ ///
+ /// - X, Y 축 라벨 커스터마이징
+ /// - 읽기 전용 모드
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
+ ///
///
///
/// C# 코드에서 사용:
@@ -44,6 +51,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Vector2(100, 50);
/// readOnlyField.IsReadOnly = true;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var sizeField = new UTKVector2Field("크기");
+ /// sizeField.ErrorMessage = "크기는 양수여야 합니다.";
+ /// sizeField.Validation = () => sizeField.Value.x > 0 && sizeField.Value.y > 0;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = sizeField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// sizeField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이)
+ /// sizeField.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// sizeField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
///
@@ -59,6 +86,9 @@ namespace UVC.UIToolkit
///
///
///
+ ///
+ ///
+ ///
///
///
/// 실제 활용 예시:
@@ -90,6 +120,9 @@ namespace UVC.UIToolkit
private bool _isReadOnly = false;
private string _xLabel = "X";
private string _yLabel = "Y";
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -154,6 +187,27 @@ namespace UVC.UIToolkit
EnableInClassList("utk-vector2-field--readonly", value);
}
}
+
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-vector2-field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
#endregion
#region Constructor
@@ -198,6 +252,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -251,6 +306,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-vector2-field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-vector2-field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-vector2-field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -262,8 +384,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs
index 30289fd3..1e80220e 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs
@@ -52,6 +52,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Vector3(1, 2, 3);
/// readOnlyField.IsReadOnly = true;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var posField = new UTKVector3Field("위치");
+ /// posField.ErrorMessage = "Y값은 0 이상이어야 합니다.";
+ /// posField.Validation = () => posField.Value.y >= 0;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = posField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// posField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// posField.ErrorMessage = "유효하지 않은 좌표입니다.";
+ /// posField.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
@@ -66,6 +86,9 @@ namespace UVC.UIToolkit
///
///
///
+ ///
+ ///
+ ///
/// ]]>
/// 실제 활용 예시 (Transform 편집기):
///
@@ -97,6 +120,9 @@ namespace UVC.UIToolkit
private string _xLabel = "X";
private string _yLabel = "Y";
private string _zLabel = "Z";
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -173,6 +199,36 @@ namespace UVC.UIToolkit
EnableInClassList("utk-vector3-field--readonly", value);
}
}
+
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-vector3-field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ ///
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시.
+ ///
+ ///
+ ///
+ /// var posField = new UTKVector3Field("위치");
+ /// posField.ErrorMessage = "Y값은 0 이상이어야 합니다.";
+ /// posField.Validation = () => posField.Value.y >= 0;
+ ///
+ ///
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
#endregion
#region Constructor
@@ -217,6 +273,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -271,6 +328,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-vector3-field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-vector3-field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-vector3-field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -282,8 +406,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs
index 418cf03c..6eddb697 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs
@@ -18,6 +18,10 @@ namespace UVC.UIToolkit
/// - 색상(RGBA): Red, Green, Blue, Alpha 값 (0~1 범위)
/// - 쉐이더 파라미터: 커스텀 쉐이더에 전달하는 4개의 값
/// - 사원수(Quaternion)와 유사한 구조로 회전 데이터 저장
+ ///
+ /// - Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)
+ /// - 에러 상태 시 붉은 외곽선 + 에러 메시지 표시
+ ///
///
///
/// C# 코드에서 사용:
@@ -48,6 +52,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Vector4(1, 0.5f, 0.5f, 1);
/// readOnlyField.IsReadOnly = true;
///
+ /// Validation (입력 검증):
+ ///
+ /// // 검증 함수 설정 (Func<bool>)
+ /// var colorField = new UTKVector4Field("색상");
+ /// colorField.ErrorMessage = "알파 값은 0~1 사이여야 합니다.";
+ /// colorField.Validation = () => colorField.Value.w >= 0 && colorField.Value.w <= 1;
+ /// // → FocusOut 시 자동으로 검증
+ /// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
+ ///
+ /// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
+ /// bool isValid = colorField.Validate();
+ /// if (!isValid) return; // 검증 실패
+ ///
+ /// // 에러 수동 해제
+ /// colorField.ClearError();
+ ///
+ /// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
+ /// vec4Field.ErrorMessage = "서버 오류가 발생했습니다.";
+ /// vec4Field.ErrorMessage = ""; // 오류 제거
+ ///
/// UXML에서 사용:
///
///
@@ -64,6 +88,9 @@ namespace UVC.UIToolkit
///
///
///
+ ///
+ ///
+ ///
///
///
/// 실제 활용 예시:
@@ -95,6 +122,9 @@ namespace UVC.UIToolkit
private string _yLabel = "Y";
private string _zLabel = "Z";
private string _wLabel = "W";
+ private string _errorMessage = "";
+ private Func? _validation;
+ private Label? _errorLabel;
#endregion
#region Events
@@ -183,6 +213,27 @@ namespace UVC.UIToolkit
EnableInClassList("utk-vector4field--readonly", value);
}
}
+
+ /// 에러 메시지. 비어있지 않으면 에러 상태로 표시
+ [UxmlAttribute("error-message")]
+ public string ErrorMessage
+ {
+ get => _errorMessage;
+ set
+ {
+ _errorMessage = value;
+ var hasError = !string.IsNullOrEmpty(value);
+ EnableInClassList("utk-vector4field--error", hasError);
+ UpdateErrorLabel(hasError ? value : null);
+ }
+ }
+
+ /// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시
+ public Func? Validation
+ {
+ get => _validation;
+ set => _validation = value;
+ }
#endregion
#region Constructor
@@ -227,6 +278,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback>(OnFieldValueChanged);
+ RegisterCallback(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -282,6 +334,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
+
+ private void OnFocusOut(FocusOutEvent evt)
+ {
+ RunValidation();
+ }
+ #endregion
+
+ #region Methods
+ ///
+ /// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
+ ///
+ /// Validation이 null이면 true, 아니면 Validation 결과
+ public bool Validate()
+ {
+ return RunValidation();
+ }
+
+ /// 에러 상태를 수동으로 해제합니다.
+ public void ClearError()
+ {
+ ErrorMessage = "";
+ }
+
+ private bool RunValidation()
+ {
+ if (_validation == null) return true;
+
+ var isValid = _validation.Invoke();
+ if (isValid)
+ {
+ // 검증 통과 시 에러 상태 해제
+ EnableInClassList("utk-vector4field--error", false);
+ UpdateErrorLabel(null);
+ }
+ else
+ {
+ // 검증 실패 시 에러 상태 표시
+ EnableInClassList("utk-vector4field--error", true);
+ UpdateErrorLabel(_errorMessage);
+ }
+ return isValid;
+ }
+
+ private void UpdateErrorLabel(string? message)
+ {
+ if (string.IsNullOrEmpty(message))
+ {
+ // 에러 라벨 숨기기 (존재하면)
+ if (_errorLabel != null)
+ {
+ _errorLabel.style.display = DisplayStyle.None;
+ }
+ return;
+ }
+
+ // 에러 라벨 생성 (지연 생성 - 필요할 때만)
+ if (_errorLabel == null)
+ {
+ _errorLabel = new Label();
+ _errorLabel.AddToClassList("utk-vector4field__error-message");
+ _errorLabel.style.display = DisplayStyle.None;
+ Add(_errorLabel);
+ }
+
+ _errorLabel.text = message;
+ _errorLabel.style.display = DisplayStyle.Flex;
+ }
#endregion
#region IDisposable
@@ -293,8 +412,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
- OnValueChanged = null;
UnregisterCallback>(OnFieldValueChanged);
+ UnregisterCallback(OnFocusOut);
+ OnValueChanged = null;
+ _validation = null;
+ _errorLabel = null;
}
#endregion
}
diff --git a/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs b/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs
index 8618c2f1..5da9992f 100644
--- a/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs
+++ b/Assets/Scripts/UVC/UIToolkit/List/UTKPropertyList.cs
@@ -976,10 +976,13 @@ namespace UVC.UIToolkit
// TreeView 갱신은 ToggleGroupExpanded에서 처리
}
- private void OnItemValueChanged(IUTKPropertyItem item, object? oldValue, object? newValue)
+ private void OnItemValueChanged(IUTKPropertyItem item, object? oldValue, object? newValue, bool notify)
{
- var args = new UTKPropertyValueChangedEventArgs(item, oldValue, newValue);
- OnPropertyValueChanged?.Invoke(args);
+ if (notify)
+ {
+ var args = new UTKPropertyValueChangedEventArgs(item, oldValue, newValue);
+ OnPropertyValueChanged?.Invoke(args);
+ }
}
private void OnSearch(string newValue)
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/Core/IUTKPropertyItem.cs b/Assets/Scripts/UVC/UIToolkit/Property/Core/IUTKPropertyItem.cs
index 1302eff0..dbd94c48 100644
--- a/Assets/Scripts/UVC/UIToolkit/Property/Core/IUTKPropertyItem.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Property/Core/IUTKPropertyItem.cs
@@ -38,7 +38,7 @@ namespace UVC.UIToolkit
string? GroupId { get; set; }
/// 값 변경 이벤트
- event Action? OnValueChanged;
+ event Action? OnValueChanged;
/// 상태(ReadOnly 등) 변경 이벤트. View가 구독하여 UI를 갱신합니다.
event Action? OnStateChanged;
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/Items/Base/UTKPropertyItemBase.cs b/Assets/Scripts/UVC/UIToolkit/Property/Items/Base/UTKPropertyItemBase.cs
index c846bd8f..cb714b98 100644
--- a/Assets/Scripts/UVC/UIToolkit/Property/Items/Base/UTKPropertyItemBase.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Property/Items/Base/UTKPropertyItemBase.cs
@@ -124,7 +124,7 @@ namespace UVC.UIToolkit
#region Events
/// 값 변경 이벤트 (object 타입)
- public event Action? OnValueChanged;
+ public event Action? OnValueChanged;
/// 값 변경 이벤트 (제네릭 타입)
public event Action, T, T>? OnTypedValueChanged;
@@ -159,35 +159,34 @@ namespace UVC.UIToolkit
{
if (default(T) == null)
{
- if (notifyChangeEvent)
- {
- Value = default!;
- }
- else
+ if (!Equals(_value, value))
{
+ var oldValue = _value;
_value = default!;
+ NotifyValueChanged(oldValue, default!, notifyChangeEvent);
}
}
}
else if (value is T typedValue)
{
- if (notifyChangeEvent)
- {
- Value = typedValue;
- }
- else
+ if (!Equals(_value, typedValue))
{
+ var oldValue = _value;
_value = typedValue;
+ NotifyValueChanged(oldValue, typedValue, notifyChangeEvent);
}
}
else
{
try
{
- if (notifyChangeEvent)
- Value = (T)Convert.ChangeType(value, typeof(T));
- else
- _value = (T)Convert.ChangeType(value, typeof(T));
+ var v = (T)Convert.ChangeType(value, typeof(T));
+ if (!Equals(_value, v))
+ {
+ var oldValue = _value;
+ _value = v;
+ NotifyValueChanged(oldValue, v, notifyChangeEvent);
+ }
}
catch (Exception ex)
{
@@ -199,10 +198,10 @@ namespace UVC.UIToolkit
#region Protected Methods
/// 값 변경을 알립니다.
- protected void NotifyValueChanged(T oldValue, T newValue)
+ protected void NotifyValueChanged(T oldValue, T newValue, bool notifyChangeEvent = false)
{
OnTypedValueChanged?.Invoke(this, oldValue, newValue);
- OnValueChanged?.Invoke(this, oldValue, newValue);
+ OnValueChanged?.Invoke(this, oldValue, newValue, notifyChangeEvent);
}
#endregion
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/Views/UTKFloatDropdownPropertyItemView.cs b/Assets/Scripts/UVC/UIToolkit/Property/Views/UTKFloatDropdownPropertyItemView.cs
index 6dfb7721..01b9cece 100644
--- a/Assets/Scripts/UVC/UIToolkit/Property/Views/UTKFloatDropdownPropertyItemView.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Property/Views/UTKFloatDropdownPropertyItemView.cs
@@ -14,7 +14,7 @@ namespace UVC.UIToolkit
/// 사용법 (Data 바인딩):
///
/// var data = new UTKFloatDropdownPropertyItem("id", "Label", 1.5f,
- /// new List<string> { "A", "B", "C" }, "A");
+ /// new List { "A", "B", "C" }, "A");
/// var view = new UTKFloatDropdownPropertyItemView();
/// view.Bind(data);
///
diff --git a/Assets/Scripts/UVC/UIToolkit/UTKThemeManager.cs b/Assets/Scripts/UVC/UIToolkit/UTKThemeManager.cs
index 693126c4..34864b62 100644
--- a/Assets/Scripts/UVC/UIToolkit/UTKThemeManager.cs
+++ b/Assets/Scripts/UVC/UIToolkit/UTKThemeManager.cs
@@ -128,8 +128,8 @@ namespace UVC.UIToolkit
/// private void SubscribeToThemeChanges()
/// {
/// UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
- /// RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
- /// RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
+ /// RegisterCallback(OnAttachToPanelForTheme);
+ /// RegisterCallback(OnDetachFromPanelForTheme);
/// }
///
/// private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
@@ -154,8 +154,8 @@ namespace UVC.UIToolkit
/// if (_disposed) return;
/// _disposed = true;
/// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
- /// UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
- /// UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
+ /// UnregisterCallback(OnAttachToPanelForTheme);
+ /// UnregisterCallback(OnDetachFromPanelForTheme);
/// }
/// }
///
diff --git a/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs
index 78cf4b84..7da16fef 100644
--- a/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs
@@ -51,7 +51,7 @@ namespace UVC.UIToolkit
/// };
///
/// // 3. 데이터 로드 (그룹 + 개별 아이템 혼합)
- /// var entries = new List<IUTKPropertyEntry>();
+ /// var entries = new List();
/// entries.Add(new UTKStringPropertyItem("name", "이름", "기본값"));
///
/// var group = new UTKPropertyGroup("transform", "Transform");