From fb78769170e6e1be179b7585463d390ab38ada3b Mon Sep 17 00:00:00 2001
From: lrc <571244399@qq.com>
Date: Sun, 26 Jul 2026 10:43:28 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AA=97=E5=8F=A3=E6=8E=A7?=
=?UTF-8?q?=E5=88=B6=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Editor/Form.meta | 8 +
Editor/Form/UIFormControllerEditor.cs | 145 ++++
Editor/Form/UIFormControllerEditor.cs.meta | 11 +
Editor/Form/UITitleBarEditor.cs | 36 +
Editor/Form/UITitleBarEditor.cs.meta | 11 +
Runtime/Form/Controllers.meta | 8 +
Runtime/Form/Controllers/AutoHideDirection.cs | 14 +
.../Controllers/AutoHideDirection.cs.meta | 11 +
Runtime/Form/Controllers/IUIFormTarget.cs | 39 ++
.../Form/Controllers/IUIFormTarget.cs.meta | 11 +
Runtime/Form/Controllers/UIFormController.cs | 647 ++++++++++++++++++
.../Form/Controllers/UIFormController.cs.meta | 11 +
Runtime/Form/Controllers/UITitleBar.cs | 131 ++++
Runtime/Form/Controllers/UITitleBar.cs.meta | 11 +
Runtime/Form/Controllers/WindowState.cs | 20 +
Runtime/Form/Controllers/WindowState.cs.meta | 11 +
Runtime/Form/UIFormManager.cs | 290 ++++++++
Runtime/Form/UIFormManager.cs.meta | 11 +
18 files changed, 1426 insertions(+)
create mode 100644 Editor/Form.meta
create mode 100644 Editor/Form/UIFormControllerEditor.cs
create mode 100644 Editor/Form/UIFormControllerEditor.cs.meta
create mode 100644 Editor/Form/UITitleBarEditor.cs
create mode 100644 Editor/Form/UITitleBarEditor.cs.meta
create mode 100644 Runtime/Form/Controllers.meta
create mode 100644 Runtime/Form/Controllers/AutoHideDirection.cs
create mode 100644 Runtime/Form/Controllers/AutoHideDirection.cs.meta
create mode 100644 Runtime/Form/Controllers/IUIFormTarget.cs
create mode 100644 Runtime/Form/Controllers/IUIFormTarget.cs.meta
create mode 100644 Runtime/Form/Controllers/UIFormController.cs
create mode 100644 Runtime/Form/Controllers/UIFormController.cs.meta
create mode 100644 Runtime/Form/Controllers/UITitleBar.cs
create mode 100644 Runtime/Form/Controllers/UITitleBar.cs.meta
create mode 100644 Runtime/Form/Controllers/WindowState.cs
create mode 100644 Runtime/Form/Controllers/WindowState.cs.meta
create mode 100644 Runtime/Form/UIFormManager.cs
create mode 100644 Runtime/Form/UIFormManager.cs.meta
diff --git a/Editor/Form.meta b/Editor/Form.meta
new file mode 100644
index 0000000..7366297
--- /dev/null
+++ b/Editor/Form.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2f18f75958efeb8428a4689e959e4350
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Form/UIFormControllerEditor.cs b/Editor/Form/UIFormControllerEditor.cs
new file mode 100644
index 0000000..fb99b95
--- /dev/null
+++ b/Editor/Form/UIFormControllerEditor.cs
@@ -0,0 +1,145 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace XericUIEditor.Form
+{
+ [CustomEditor(typeof(XericUI.Form.UIFormController))]
+ public class UIFormControllerEditor : Editor
+ {
+ private XericUI.Form.UIFormController _controller;
+
+ private void OnEnable()
+ {
+ _controller = (XericUI.Form.UIFormController)target;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+
+ DrawBasicSection();
+ DrawEdgeSection();
+ DrawDebugSection();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ #region 基本设置
+
+ private void DrawBasicSection()
+ {
+ EditorGUILayout.LabelField("窗口基本设置", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_WindowTitle"), new GUIContent("窗口标题 (Title)"));
+ EditorGUILayout.Space();
+ }
+
+ #endregion
+
+ #region 边缘行为
+
+ private void DrawEdgeSection()
+ {
+ EditorGUILayout.LabelField("边缘吸附", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_EnableAutoSnap"), new GUIContent("启用边缘吸附 (Auto Snap)"));
+ if (_controller.AutoSnapEnabled)
+ {
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_SnapThreshold"), new GUIContent("吸附阈值 (Snap Threshold, px)"));
+ }
+
+ EditorGUILayout.Space();
+
+ EditorGUILayout.LabelField("自动隐藏", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_EnableAutoHide"), new GUIContent("启用自动隐藏 (Auto Hide)"));
+ if (_controller.AutoHideEnabled)
+ {
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_AutoHideDirection"), new GUIContent("隐藏方向 (Direction)"));
+ }
+
+ EditorGUILayout.Space();
+ }
+
+ #endregion
+
+ #region 调试与测试
+
+ private void DrawDebugSection()
+ {
+ EditorGUILayout.LabelField("调试", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_DebugMode"), new GUIContent("调试模式 (Debug Mode)"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_DebugLogEvents"), new GUIContent("事件日志 (Log Events)"));
+
+ EditorGUILayout.Space();
+
+ // 当前状态
+ EditorGUILayout.LabelField("当前状态", EditorStyles.boldLabel);
+
+ var state = _controller.CurrentState;
+ Color stateColor;
+ string stateText;
+
+ switch (state)
+ {
+ case XericUI.Form.WindowState.Normal:
+ stateColor = Color.white;
+ stateText = "● 窗口化 (Normal)";
+ break;
+ case XericUI.Form.WindowState.Maximized:
+ stateColor = Color.green;
+ stateText = "■ 全屏 (Maximized)";
+ break;
+ case XericUI.Form.WindowState.Minimized:
+ stateColor = Color.gray;
+ stateText = "○ 最小化 (Minimized)";
+ break;
+ case XericUI.Form.WindowState.EdgeHidden:
+ stateColor = new Color(1f, 0.6f, 0f);
+ stateText = "◄ 边缘隐藏 (Edge Hidden)";
+ break;
+ default:
+ stateColor = Color.white;
+ stateText = state.ToString();
+ break;
+ }
+
+ GUI.color = stateColor;
+ EditorGUILayout.LabelField(stateText, EditorStyles.miniBoldLabel);
+ GUI.color = Color.white;
+
+ EditorGUILayout.Space();
+
+ // 手动测试按钮
+ EditorGUILayout.LabelField("手动测试", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("全屏/还原"))
+ {
+ _controller.ToggleMaximize();
+ SceneView.RepaintAll();
+ }
+ if (GUILayout.Button("模拟进入"))
+ {
+ _controller.SimulateEnter();
+ SceneView.RepaintAll();
+ }
+ if (GUILayout.Button("模拟退出"))
+ {
+ _controller.SimulateExit();
+ SceneView.RepaintAll();
+ }
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space();
+
+ // 调试提示
+ if (!Application.isPlaying && _controller.DebugMode)
+ {
+ EditorGUILayout.HelpBox(
+ "调试模式已启用 — 在 SceneView 中移动鼠标即可预览。\n" +
+ "蓝色边框 = 窗口范围 | 绿色 = 鼠标悬停中 | 橙色 = 吸附阈值范围",
+ MessageType.Info);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Editor/Form/UIFormControllerEditor.cs.meta b/Editor/Form/UIFormControllerEditor.cs.meta
new file mode 100644
index 0000000..9488ea3
--- /dev/null
+++ b/Editor/Form/UIFormControllerEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9f105e0b75b54e142b3844d8f657e32a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Form/UITitleBarEditor.cs b/Editor/Form/UITitleBarEditor.cs
new file mode 100644
index 0000000..e458dd0
--- /dev/null
+++ b/Editor/Form/UITitleBarEditor.cs
@@ -0,0 +1,36 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace XericUIEditor.Form
+{
+ [CustomEditor(typeof(XericUI.Form.UITitleBar))]
+ public class UITitleBarEditor : Editor
+ {
+ private XericUI.Form.UITitleBar _titleBar;
+
+ private void OnEnable()
+ {
+ _titleBar = (XericUI.Form.UITitleBar)target;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+
+ EditorGUILayout.LabelField("标题栏控件引用", EditorStyles.boldLabel);
+ EditorGUILayout.HelpBox(
+ "将标题栏下的子控件拖入下方槽位即可。\n" +
+ "UITitleBar 会在 Start 时自动向上查找父级的 UIFormController 并绑定事件。",
+ MessageType.Info);
+
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_TitleText"), new GUIContent("标题文本 (Title Text)"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_CloseButton"), new GUIContent("关闭按钮 (Close)"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_MaximizeButton"), new GUIContent("最大化按钮 (Maximize)"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_MinimizeButton"), new GUIContent("最小化按钮 (Minimize)"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_AutoSnapToggle"), new GUIContent("吸附开关 (Auto Snap)"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_AutoHideToggle"), new GUIContent("隐藏开关 (Auto Hide)"));
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+}
diff --git a/Editor/Form/UITitleBarEditor.cs.meta b/Editor/Form/UITitleBarEditor.cs.meta
new file mode 100644
index 0000000..abe633b
--- /dev/null
+++ b/Editor/Form/UITitleBarEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5c45b2d7be6212e479b5cf00a0369596
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Form/Controllers.meta b/Runtime/Form/Controllers.meta
new file mode 100644
index 0000000..5b0f65a
--- /dev/null
+++ b/Runtime/Form/Controllers.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d1e580c430b3308419647d63c73b21c2
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Form/Controllers/AutoHideDirection.cs b/Runtime/Form/Controllers/AutoHideDirection.cs
new file mode 100644
index 0000000..c3dab5c
--- /dev/null
+++ b/Runtime/Form/Controllers/AutoHideDirection.cs
@@ -0,0 +1,14 @@
+namespace XericUI.Form
+{
+ ///
+ /// 自动隐藏滑动方向。
+ /// 窗口贴边后可沿此方向滑出屏幕,鼠标进入对应边缘区域后滑回。
+ ///
+ public enum AutoHideDirection
+ {
+ Left,
+ Right,
+ Up,
+ Down
+ }
+}
diff --git a/Runtime/Form/Controllers/AutoHideDirection.cs.meta b/Runtime/Form/Controllers/AutoHideDirection.cs.meta
new file mode 100644
index 0000000..a26cdbd
--- /dev/null
+++ b/Runtime/Form/Controllers/AutoHideDirection.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6441e2d1b03e7a942a1c958f979e9d3d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Form/Controllers/IUIFormTarget.cs b/Runtime/Form/Controllers/IUIFormTarget.cs
new file mode 100644
index 0000000..5d50570
--- /dev/null
+++ b/Runtime/Form/Controllers/IUIFormTarget.cs
@@ -0,0 +1,39 @@
+using UnityEngine;
+
+namespace XericUI.Form
+{
+ ///
+ /// 窗体悬停目标接口。
+ /// 实现此接口的组件可注册到 UIFormManager,由管理器统一检测鼠标悬停状态。
+ ///
+ public interface IUIFormTarget
+ {
+ ///
+ /// 进入范围(屏幕空间 Rect,原点为左下角,单位为像素)。
+ /// 鼠标进入此范围时触发 OnPointerEnter。
+ ///
+ Rect GetEnterRect();
+
+ ///
+ /// 退出范围(屏幕空间 Rect,原点为左下角,单位为像素)。
+ /// 应大于等于 EnterRect,鼠标离开此范围时触发 OnPointerExit,避免边界抖动。
+ ///
+ Rect GetExitRect();
+
+ ///
+ /// 鼠标进入范围内时由 UIFormManager 调用。
+ ///
+ void OnPointerEnter();
+
+ ///
+ /// 鼠标离开退出范围时由 UIFormManager 调用。
+ ///
+ void OnPointerExit();
+
+ ///
+ /// 此目标是否处于激活状态。
+ /// 返回 false 时管理器跳过此目标的检测。
+ ///
+ bool IsActive { get; }
+ }
+}
diff --git a/Runtime/Form/Controllers/IUIFormTarget.cs.meta b/Runtime/Form/Controllers/IUIFormTarget.cs.meta
new file mode 100644
index 0000000..94bc4cc
--- /dev/null
+++ b/Runtime/Form/Controllers/IUIFormTarget.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 756fa65ea23a49d4ea270f1697aaf89f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Form/Controllers/UIFormController.cs b/Runtime/Form/Controllers/UIFormController.cs
new file mode 100644
index 0000000..7ba8998
--- /dev/null
+++ b/Runtime/Form/Controllers/UIFormController.cs
@@ -0,0 +1,647 @@
+using System.Collections;
+using UnityEngine;
+using UnityEngine.EventSystems;
+using UnityEngine.UI;
+
+namespace XericUI.Form
+{
+ #region 枚举与结构体
+
+ /// 范围坐标空间枚举
+ public enum FormRangeSpace
+ {
+ RelativeOffset,
+ CanvasLocal,
+ AnchorToSelf,
+ AnchorToCanvas
+ }
+
+ [System.Serializable]
+ public struct RangeRect
+ {
+ [Tooltip("坐标空间")]
+ public FormRangeSpace space;
+
+ [Tooltip("范围定义。\nRelativeOffset: x,y=像素偏移; w,h=像素宽高(0=用参考尺寸)\nCanvasLocal: Canvas局部坐标\nAnchorToSelf: x,y=锚点0-1; w,h=尺寸比例0-1\nAnchorToCanvas: x,y=锚点0-1; w,h=尺寸比例0-1")]
+ public Rect rect;
+ }
+
+ #endregion
+
+ ///
+ /// UI 窗体控制器 — 挂载到窗口根 GameObject 上,自身即为完整窗口。
+ /// 支持全屏/还原、边缘吸附、自动隐藏、拖拽移动。
+ /// 标题栏功能由独立的 UITitleBar 子组件提供,Controller 只提供公开 API。
+ ///
+ [AddComponentMenu("Xeric UI Vessel/Form/UI Form Controller")]
+ [RequireComponent(typeof(RectTransform))]
+ public class UIFormController : MonoBehaviour, IUIFormTarget, IBeginDragHandler, IDragHandler, IEndDragHandler
+ {
+ #region 序列化字段
+
+ [Header("窗口基本设置")]
+ [SerializeField, Tooltip("窗口标题,供外部读取")]
+ private string m_WindowTitle = "Window";
+
+ [Header("边缘吸附")]
+ [SerializeField, Tooltip("是否启用边缘自动吸附")]
+ private bool m_EnableAutoSnap = true;
+
+ [SerializeField, Tooltip("吸附阈值(像素),窗口距边缘小于此值时自动贴边")]
+ private float m_SnapThreshold = 8f;
+
+ [Header("自动隐藏")]
+ [SerializeField, Tooltip("是否启用自动隐藏(贴边后滑出屏幕)")]
+ private bool m_EnableAutoHide;
+
+ [SerializeField, Tooltip("自动隐藏的滑动方向")]
+ private AutoHideDirection m_AutoHideDirection = AutoHideDirection.Left;
+
+ [Header("调试")]
+ [SerializeField, Tooltip("编辑器调试模式")]
+ private bool m_DebugMode = true;
+
+ [SerializeField, Tooltip("输出事件日志")]
+ private bool m_DebugLogEvents;
+
+ #endregion
+
+ #region 公开属性
+
+ public string WindowTitle => m_WindowTitle;
+
+ /// 是否处于全屏状态
+ public bool IsMaximized => _windowState == WindowState.Maximized;
+
+ /// 是否处于边缘隐藏状态
+ public bool IsEdgeHidden => _windowState == WindowState.EdgeHidden;
+
+ /// 当前窗口状态
+ public WindowState CurrentState => _windowState;
+
+ /// 自动吸附是否开启
+ public bool AutoSnapEnabled => m_EnableAutoSnap;
+
+ /// 自动隐藏是否开启
+ public bool AutoHideEnabled => m_EnableAutoHide;
+
+ /// 自动隐藏方向
+ public AutoHideDirection HideDirection => m_AutoHideDirection;
+
+ /// 编辑器调试模式
+ public bool DebugMode => m_DebugMode;
+
+ #endregion
+
+ #region 内部状态
+
+ private RectTransform _rectTransform;
+ private Canvas _canvas;
+ private RectTransform _canvasRt;
+
+ private WindowState _windowState = WindowState.Normal;
+
+ // 拖拽
+ private Vector2 _dragOffset;
+
+ // 全屏还原数据
+ private Vector2 _savePos;
+ private Vector2 _saveSize;
+ private Vector2 _saveAnchorMin;
+ private Vector2 _saveAnchorMax;
+
+ // 边缘隐藏
+ private Vector2 _edgeVisiblePos;
+ private Vector2 _edgeHiddenPos;
+ private Coroutine _autoHideCoroutine;
+
+ // 悬停
+ private bool _isHovering;
+
+ #endregion
+
+ #region 生命周期
+
+ protected virtual void Awake()
+ {
+ _rectTransform = (RectTransform)transform;
+ _canvas = GetComponentInParent