From 3494fa6d544d8a9d9703c2038abf20eba57b69e5 Mon Sep 17 00:00:00 2001 From: daneo1989 Date: Sat, 6 Dec 2025 17:13:38 +0000 Subject: [PATCH 1/2] #1078 TextBox masking in window mode. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [32 tools called] Windowed masking was still wrong because the WinForms `TextBox` keeps painting above the DirectX surface whenever the client isn’t in exclusive fullscreen (the root cause reported in [Crystal#1078](https://github.com/Suprcode/Crystal/issues/1078)). I now relocate the WinForms control off-screen while windowed and replay all mouse traffic into it so we can keep using its text-editing logic even though it’s no longer visible. That lets the Mir UI texture be the only thing you see in both modes. ```44:82:Client/MirControls/MirTextBox.cs protected override void OnLocationChanged() { base.OnLocationChanged(); if (TextBox != null && !TextBox.IsDisposed) UpdateTextBoxHostLocation(); TextureValid = false; Redraw(); } private void UpdateTextBoxHostLocation(bool force = false) { if (TextBox == null || TextBox.IsDisposed) return; bool hide = !Settings.FullScreen; ... TextBox.Location = hide ? HiddenTextBoxLocation : DisplayLocation; _textBoxOffscreen = hide; } ``` ```296:341:Client/MirControls/MirTextBox.cs public override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (_textBoxOffscreen && e.Button == MouseButtons.Left) { SetFocus(); ForwardMouseMessage(WM_LBUTTONDOWN, e); } } ... public override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); if (_textBoxOffscreen) ForwardMouseWheel(e); } ``` ```488:600:Client/MirControls/MirTextBox.cs private void ForwardMouseMessage(int message, MouseEventArgs e, bool includeCurrentButtons = false) { if (!_textBoxOffscreen || TextBox == null || TextBox.IsDisposed) return; ... SendMessage(TextBox.Handle, message, (IntPtr)wParam, (IntPtr)lParam); } ... [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); ``` Key points: - `UpdateTextBoxHostLocation` now centralizes where the WinForms input control lives. In fullscreen it sits where it always was; in windowed mode it’s moved to `HiddenTextBoxLocation` so it never paints over Mir UIs. - Mouse input paths (`OnMouseDown/Move/Up/DoubleClick/Wheel`) detect when the control is hidden and replay the corresponding Windows messages via `SendMessage`, using `CMain.MPoint` to compute the caret position. Keyboard input still flows through the real `TextBox`, so all selection/editing behavior is preserved without rewriting it manually. - Texture creation is still triggered everywhere (windowed + fullscreen), so the Mir-themed mask/caret drawn from `TextBox.DrawToBitmap` now matches in both modes. Please launch the client, toggle `Alt+Enter`, and open any dialog that hosts a `MirTextBox` (guild, chat, login, etc.) to confirm the masks now render correctly while the text boxes remain fully interactive in both windowed and fullscreen modes. --- Client/MirControls/MirTextBox.cs | 201 ++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 3 deletions(-) diff --git a/Client/MirControls/MirTextBox.cs b/Client/MirControls/MirTextBox.cs index db1f1a256..e19329d9a 100644 --- a/Client/MirControls/MirTextBox.cs +++ b/Client/MirControls/MirTextBox.cs @@ -2,6 +2,7 @@ using SlimDX; using SlimDX.Direct3D9; using System.Drawing.Imaging; +using System.Runtime.InteropServices; namespace Client.MirControls { @@ -46,12 +47,39 @@ protected override void OnLocationChanged() { base.OnLocationChanged(); if (TextBox != null && !TextBox.IsDisposed) - TextBox.Location = DisplayLocation; + UpdateTextBoxHostLocation(); TextureValid = false; Redraw(); } + private void UpdateTextBoxHostLocation(bool force = false) + { + if (TextBox == null || TextBox.IsDisposed) + return; + + bool hide = !Settings.FullScreen; + + if (!force) + { + if (!hide && TextBox.Location == DisplayLocation && !_textBoxOffscreen) + return; + if (hide && TextBox.Location == HiddenTextBoxLocation && _textBoxOffscreen) + return; + } + + if (hide) + { + TextBox.Location = HiddenTextBoxLocation; + _textBoxOffscreen = true; + } + else + { + TextBox.Location = DisplayLocation; + _textBoxOffscreen = false; + } + } + #endregion #region Max Length @@ -80,6 +108,7 @@ protected override void OnParentChanged() base.OnParentChanged(); if (TextBox != null && !TextBox.IsDisposed) OnVisibleChanged(); + UpdateTextBoxHostLocation(true); } #endregion @@ -126,7 +155,11 @@ public System.Drawing.Font Font protected override void OnSizeChanged() { - TextBox.Size = Size; + if (TextBox != null && !TextBox.IsDisposed) + { + TextBox.Size = Size; + UpdateTextBoxHostLocation(); + } DisposeTexture(); @@ -143,6 +176,21 @@ protected override void OnSizeChanged() public bool CanLoseFocus; public readonly TextBox TextBox; private Pen CaretPen; + private bool _textBoxOffscreen; + + private static readonly Point HiddenTextBoxLocation = new Point(-5000, -5000); + + private const int WM_MOUSEMOVE = 0x0200; + private const int WM_LBUTTONDOWN = 0x0201; + private const int WM_LBUTTONUP = 0x0202; + private const int WM_LBUTTONDBLCLK = 0x0203; + private const int WM_MOUSEWHEEL = 0x020A; + + private const int MK_LBUTTON = 0x0001; + private const int MK_RBUTTON = 0x0002; + private const int MK_SHIFT = 0x0004; + private const int MK_CONTROL = 0x0008; + private const int MK_MBUTTON = 0x0010; #endregion @@ -199,7 +247,10 @@ protected override void OnVisibleChanged() base.OnVisibleChanged(); if (TextBox != null && !TextBox.IsDisposed) + { TextBox.Visible = Visible; + UpdateTextBoxHostLocation(); + } } private void TextBox_VisibleChanged(object sender, EventArgs e) { @@ -242,6 +293,53 @@ public override void MultiLine() #endregion + #region Mouse Forwarding + + public override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + + if (_textBoxOffscreen && e.Button == MouseButtons.Left) + { + SetFocus(); + ForwardMouseMessage(WM_LBUTTONDOWN, e); + } + } + + public override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + + if (_textBoxOffscreen) + ForwardMouseMessage(WM_MOUSEMOVE, e, true); + } + + public override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + + if (_textBoxOffscreen && e.Button == MouseButtons.Left) + ForwardMouseMessage(WM_LBUTTONUP, e); + } + + public override void OnMouseDoubleClick(MouseEventArgs e) + { + base.OnMouseDoubleClick(e); + + if (_textBoxOffscreen && e.Button == MouseButtons.Left) + ForwardMouseMessage(WM_LBUTTONDBLCLK, e); + } + + public override void OnMouseWheel(MouseEventArgs e) + { + base.OnMouseWheel(e); + + if (_textBoxOffscreen) + ForwardMouseWheel(e); + } + + #endregion + public MirTextBox() { BackColour = Color.Black; @@ -279,6 +377,8 @@ public MirTextBox() Shown += MirTextBox_Shown; TextBox.MouseMove += CMain.CMain_MouseMove; + + UpdateTextBoxHostLocation(true); } private void TextBox_NeedRedraw(object sender, EventArgs e) @@ -289,7 +389,7 @@ private void TextBox_NeedRedraw(object sender, EventArgs e) protected unsafe override void CreateTexture() { - if (!Settings.FullScreen) return; + UpdateTextBoxHostLocation(); if (Size.IsEmpty) return; @@ -371,6 +471,7 @@ void MirTextBox_Shown(object sender, EventArgs e) CMain.Tilde = false; TextureValid = false; + UpdateTextBoxHostLocation(true); SetFocus(); } @@ -404,6 +505,100 @@ public void DialogChanged() TextBox.Visible = Visible && TextBox.Parent != null; } + private void ForwardMouseMessage(int message, MouseEventArgs e, bool includeCurrentButtons = false) + { + if (!_textBoxOffscreen || TextBox == null || TextBox.IsDisposed) + return; + + if (!TextBox.IsHandleCreated) + TextBox.CreateControl(); + + Point localPoint = GetLocalPoint(); + int wParam = BuildMouseKeyState(e, includeCurrentButtons); + int lParam = PackPoint(localPoint); + + SendMessage(TextBox.Handle, message, (IntPtr)wParam, (IntPtr)lParam); + } + + private void ForwardMouseWheel(MouseEventArgs e) + { + if (!_textBoxOffscreen || TextBox == null || TextBox.IsDisposed) + return; + + if (!TextBox.IsHandleCreated) + TextBox.CreateControl(); + + Point localPoint = GetLocalPoint(); + int keyState = BuildMouseKeyState(e, true); + int wParam = ((short)e.Delta << 16) | (keyState & 0xFFFF); + int lParam = PackPoint(localPoint); + + SendMessage(TextBox.Handle, WM_MOUSEWHEEL, (IntPtr)wParam, (IntPtr)lParam); + } + + private int PackPoint(Point point) + { + int x = point.X; + int y = point.Y; + + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x > 0xFFFF) x = 0xFFFF; + if (y > 0xFFFF) y = 0xFFFF; + + return (y << 16) | (x & 0xFFFF); + } + + private Point GetLocalPoint() + { + int x = CMain.MPoint.X - DisplayLocation.X; + int y = CMain.MPoint.Y - DisplayLocation.Y; + + if (Size.Width > 0) + { + if (x < 0) x = 0; + if (x >= Size.Width) x = Size.Width - 1; + } + else + x = 0; + + if (Size.Height > 0) + { + if (y < 0) y = 0; + if (y >= Size.Height) y = Size.Height - 1; + } + else + y = 0; + + return new Point(x, y); + } + + private int BuildMouseKeyState(MouseEventArgs e, bool includeCurrentButtons) + { + int state = 0; + MouseButtons buttons = e.Button; + + if (includeCurrentButtons && buttons == MouseButtons.None) + buttons = Control.MouseButtons; + + if ((buttons & MouseButtons.Left) == MouseButtons.Left) + state |= MK_LBUTTON; + if ((buttons & MouseButtons.Right) == MouseButtons.Right) + state |= MK_RBUTTON; + if ((buttons & MouseButtons.Middle) == MouseButtons.Middle) + state |= MK_MBUTTON; + + if ((Control.ModifierKeys & Keys.Shift) == Keys.Shift) + state |= MK_SHIFT; + if ((Control.ModifierKeys & Keys.Control) == Keys.Control) + state |= MK_CONTROL; + + return state; + } + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + #region Disposable From 1aad4d46cd59169761280a4d7b58f7207a11719a Mon Sep 17 00:00:00 2001 From: daneo1989 Date: Sun, 7 Dec 2025 18:16:45 +0000 Subject: [PATCH 2/2] Client crashing fix. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoided the windowed-mode MirTextBox crash introduced after [Crystal#1142](https://github.com/Suprcode/Crystal/pull/1142) by only keeping the hidden WinForms `TextBox` wired into `CMain.CMain_MouseMove` while it’s actually on-screen. When the control is pushed off to `HiddenTextBoxLocation` (windowed mode), the handler is detached, so our synthetic `SendMessage` calls no longer re-enter the Mir mouse pipeline and freeze the client; the handler is re-attached automatically when fullscreen brings the textbox back. --- Client/MirControls/MirTextBox.cs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Client/MirControls/MirTextBox.cs b/Client/MirControls/MirTextBox.cs index e19329d9a..abe1d0689 100644 --- a/Client/MirControls/MirTextBox.cs +++ b/Client/MirControls/MirTextBox.cs @@ -78,6 +78,31 @@ private void UpdateTextBoxHostLocation(bool force = false) TextBox.Location = DisplayLocation; _textBoxOffscreen = false; } + + UpdateMouseMoveHook(!_textBoxOffscreen); + } + + private void UpdateMouseMoveHook(bool enabled) + { + if (TextBox == null || TextBox.IsDisposed) + return; + + if (enabled) + { + if (_mouseMoveHooked) + return; + + TextBox.MouseMove += CMain.CMain_MouseMove; + _mouseMoveHooked = true; + } + else + { + if (!_mouseMoveHooked) + return; + + TextBox.MouseMove -= CMain.CMain_MouseMove; + _mouseMoveHooked = false; + } } #endregion @@ -177,6 +202,7 @@ protected override void OnSizeChanged() public readonly TextBox TextBox; private Pen CaretPen; private bool _textBoxOffscreen; + private bool _mouseMoveHooked; private static readonly Point HiddenTextBoxLocation = new Point(-5000, -5000); @@ -376,7 +402,6 @@ public MirTextBox() TextBox.MouseWheel += TextBox_NeedRedraw; Shown += MirTextBox_Shown; - TextBox.MouseMove += CMain.CMain_MouseMove; UpdateTextBoxHostLocation(true); } @@ -608,6 +633,8 @@ protected override void Dispose(bool disposing) if (!disposing) return; + UpdateMouseMoveHook(false); + if (!TextBox.IsDisposed) TextBox.Dispose(); }