diff --git a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs
new file mode 100644
index 0000000000..d0a9c97ff9
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs
@@ -0,0 +1,151 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+using System.Windows.Controls;
+using Microsoft.Xaml.Behaviors;
+
+namespace Stride.Core.Assets.Editor.View.Behaviors
+{
+ ///
+ /// Live input mask for Guid text: re-groups hex input as 8-4-4-4-12 while typing, inserting
+ /// the dashes automatically (including right after the 8th/12th/16th/20th hex digit, so the
+ /// caret is already past the dash for the next group). Deletions are left alone — removing a
+ /// dash with backspace doesn't fight the user by re-appending it. Input that isn't plain
+ /// hex/dashes (braces, garbage) is left untouched; commit-time validation deals with it.
+ ///
+ public class GuidInputMaskBehavior : Behavior
+ {
+ private const int HexDigits = 32;
+ private const int FullLength = 36; // 32 hex digits + 4 dashes
+ private static readonly int[] GroupSizes = [8, 4, 4, 4, 12];
+
+ private bool updating;
+ private int previousLength;
+
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+ // A formatted Guid is exactly 36 chars: once full, further typing is rejected natively
+ // (replacing a selection still works). Programmatic sets are unaffected.
+ AssociatedObject.MaxLength = FullLength;
+ previousLength = AssociatedObject.Text?.Length ?? 0;
+ AssociatedObject.TextChanged += OnTextChanged;
+ }
+
+ protected override void OnDetaching()
+ {
+ AssociatedObject.TextChanged -= OnTextChanged;
+ base.OnDetaching();
+ }
+
+ private void OnTextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (updating)
+ return;
+
+ var box = AssociatedObject;
+ var text = box.Text ?? string.Empty;
+ var grew = text.Length > previousLength;
+ previousLength = text.Length;
+
+ // Also tolerate the brace/paren Guid forms on paste ("{...}", "(...)"): the wrapper
+ // characters are dropped so the content still fits the mask.
+ var raw = text.Replace("-", "").Trim('{', '}', '(', ')', ' ');
+ var valid = raw.Length <= HexDigits && IsHex(raw);
+ SetInvalidHint(!valid);
+ if (!valid)
+ return;
+
+ var formatted = Format(raw, appendTrailingDash: grew);
+ if (formatted == text)
+ return;
+
+ var rawBeforeCaret = CountHexBefore(text, box.CaretIndex);
+
+ updating = true;
+ try
+ {
+ // SetCurrentValue keeps the Text binding alive (a plain Text= would clear it).
+ box.SetCurrentValue(TextBox.TextProperty, formatted);
+ box.CaretIndex = CaretAfter(formatted, rawBeforeCaret, grew);
+ previousLength = formatted.Length;
+ }
+ finally
+ {
+ updating = false;
+ }
+ }
+
+ ///
+ /// Live feedback while typing: non-hex content turns the text red immediately, without
+ /// blocking or rewriting the input — commit-time validation still does the hard reject.
+ ///
+ private void SetInvalidHint(bool invalid)
+ {
+ if (invalid)
+ AssociatedObject.SetCurrentValue(System.Windows.Controls.Control.ForegroundProperty, System.Windows.Media.Brushes.IndianRed);
+ else
+ AssociatedObject.InvalidateProperty(System.Windows.Controls.Control.ForegroundProperty);
+ }
+
+ private static bool IsHex(string s)
+ {
+ foreach (var c in s)
+ {
+ if (!char.IsAsciiHexDigit(c))
+ return false;
+ }
+ return true;
+ }
+
+ private static string Format(string raw, bool appendTrailingDash)
+ {
+ var result = new System.Text.StringBuilder(raw.Length + 4);
+ int taken = 0;
+ foreach (var size in GroupSizes)
+ {
+ if (taken >= raw.Length)
+ break;
+
+ var count = System.Math.Min(size, raw.Length - taken);
+ result.Append(raw, taken, count);
+ taken += count;
+
+ var groupFull = count == size && taken < HexDigits;
+ // Dash between groups when more digits follow; trailing dash only on growth,
+ // so backspacing over a dash doesn't immediately re-append it.
+ if (groupFull && (taken < raw.Length || appendTrailingDash))
+ result.Append('-');
+ }
+ return result.ToString();
+ }
+
+ private static int CountHexBefore(string text, int caret)
+ {
+ int count = 0;
+ for (int i = 0; i < caret && i < text.Length; i++)
+ {
+ if (text[i] != '-')
+ count++;
+ }
+ return count;
+ }
+
+ private static int CaretAfter(string formatted, int rawBefore, bool grew)
+ {
+ int i = 0, seen = 0;
+ while (i < formatted.Length && seen < rawBefore)
+ {
+ if (formatted[i] != '-')
+ seen++;
+ i++;
+ }
+ // After typing, hop over the dash we just inserted so the next digit starts the new group.
+ if (grew)
+ {
+ while (i < formatted.Length && formatted[i] == '-')
+ i++;
+ }
+ return i;
+ }
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
index ac4752ef2a..492e181024 100644
--- a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
+++ b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml
@@ -1009,6 +1009,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs
new file mode 100644
index 0000000000..1ec40daa37
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs
@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+using System;
+using System.Globalization;
+using Stride.Core.Presentation.ValueConverters;
+
+namespace Stride.Core.Assets.Editor.View.ValueConverters
+{
+ ///
+ /// Ghost-text helper for the Guid editor: given the text typed so far, returns the rest of the
+ /// canonical Guid template ("00000000-0000-0000-0000-000000000000"), so the editor can show
+ /// inline how many characters (and which dashes) are still missing. Empty once the input
+ /// reaches the full template length.
+ ///
+ public class GuidTemplateRemainder : OneWayValueConverter
+ {
+ private const string Template = "00000000-0000-0000-0000-000000000000";
+
+ public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var text = value as string ?? string.Empty;
+ return text.Length < Template.Length ? Template[text.Length..] : string.Empty;
+ }
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs
new file mode 100644
index 0000000000..11a9ba5510
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs
@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+using System;
+using System.Globalization;
+using Stride.Core.Presentation.Quantum.ViewModels;
+using Stride.Core.Presentation.ValueConverters;
+
+namespace Stride.Core.Assets.Editor.View.ValueConverters
+{
+ public class GuidToString : ValueConverterBase
+ {
+ public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value != NodeViewModel.DifferentValues ? value?.ToString() ?? string.Empty : null;
+ }
+
+ public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // Invalid text is passed through untouched: the strongly-typed node setter then throws
+ // InvalidCastException, which TextBoxBase turns into TextToSourceValueConversionFailed —
+ // the validation behavior shows its error adorner and the text reverts to the source value.
+ return Guid.TryParse(value as string, out var guid) ? guid : value;
+ }
+ }
+}