Forge
csharpdeeb25a2
1using System.Diagnostics.CodeAnalysis;
2using Avalonia;
3using Avalonia.Controls;
4using Avalonia.Platform;
5using CascadeIDE.Models;
6using CascadeIDE.Services.Presentation;
7
8namespace CascadeIDE.Services;
9
10/// <summary>
11/// Размещение вторичных <c>TopLevel</c> по презентации (ADR 0017): целевой экран по индексу,
12/// восстановление из <c>settings.toml</c> с прижатием к рабочим областям. Общий код для PFD/MFD-хостов.
13/// </summary>
14public static class PresentationHostWindowPlacement
15{
16 private const double MinRestoreWidth = 320;
17 private const double MinRestoreHeight = 240;
18
19 /// <summary>Геометрия из настроек (все четыре поля заданы). Позиция — пиксели; ширина/высота — DIP (как <see cref="Window.Width"/>).</summary>
20 public readonly record struct PresentationHostWindowBounds(int PixelX, int PixelY, double Width, double Height);
21
22 /// <summary>Восстановить из <paramref name="saved"/> при пересечении с рабочей областью; иначе — <see cref="PlaceNearMain"/>.</summary>
23 /// <param name="presentationScreenIndex">Индекс дисплея в порядке <see cref="PresentationMonitorTopology.OrderScreensForPresentation"/>; <c>null</c> — эвристика «не тот же, что главное окно».</param>
24 /// <param name="maximizeOnDedicatedScreens">См. <see cref="DisplaySettings.MaximizePresentationHostWindowsOnDedicatedScreens"/>.</param>
25 public static void PlaceOrRestore(
26 Window mainWindow,
27 Window hostWindow,
28 PresentationHostWindowBounds? saved,
29 int? presentationScreenIndex = null,
30 bool maximizeOnDedicatedScreens = true)
31 {
32 if (saved is { } b && TryClampToWorkingAreas(mainWindow, b, out var pos, out var w, out var h, out var matchedScreen))
33 {
34 if (presentationScreenIndex is >= 0 and var tIdx)
35 {
36 var screens = mainWindow.Screens?.All;
37 if (screens is { Count: >= 2 })
38 {
39 var ordered = PresentationMonitorTopology.OrderScreensForPresentation(screens);
40 if (tIdx < ordered.Count && matchedScreen is not null
41 && !ReferenceEquals(matchedScreen, ordered[tIdx]))
42 {
43 PlaceNearMain(mainWindow, hostWindow, presentationScreenIndex, maximizeOnDedicatedScreens);
44 return;
45 }
46
47 if (tIdx < ordered.Count && matchedScreen is not null
48 && ReferenceEquals(matchedScreen, ordered[tIdx]))
49 {
50 PresentHostOnDedicatedScreen(hostWindow, matchedScreen, maximizeOnDedicatedScreens);
51 return;
52 }
53 }
54 }
55
56 hostWindow.Position = pos;
57 hostWindow.Width = w;
58 hostWindow.Height = h;
59 return;
60 }
61
62 PlaceNearMain(mainWindow, hostWindow, presentationScreenIndex, maximizeOnDedicatedScreens);
63 }
64
65 /// <param name="presentationScreenIndex">См. <see cref="PlaceOrRestore"/>.</param>
66 /// <param name="maximizeOnDedicatedScreens">См. <see cref="PlaceOrRestore"/>.</param>
67 public static void PlaceNearMain(
68 Window mainWindow,
69 Window hostWindow,
70 int? presentationScreenIndex = null,
71 bool maximizeOnDedicatedScreens = true)
72 {
73 var screens = mainWindow.Screens?.All;
74 if (screens is null || screens.Count == 0)
75 {
76 hostWindow.Position = new PixelPoint(mainWindow.Position.X + 48, mainWindow.Position.Y + 48);
77 return;
78 }
79
80 if (screens.Count < 2)
81 {
82 hostWindow.Position = new PixelPoint(mainWindow.Position.X + 48, mainWindow.Position.Y + 48);
83 return;
84 }
85
86 if (presentationScreenIndex is >= 0 and var targetIdx)
87 {
88 var ordered = PresentationMonitorTopology.OrderScreensForPresentation(screens);
89 if (targetIdx < ordered.Count)
90 {
91 PresentHostOnDedicatedScreen(hostWindow, ordered[targetIdx], maximizeOnDedicatedScreens);
92 return;
93 }
94 }
95
96 var mainScreen = mainWindow.Screens?.ScreenFromWindow(mainWindow);
97 Screen? candidate = null;
98 foreach (var sc in screens)
99 {
100 if (ReferenceEquals(sc, mainScreen))
101 continue;
102 candidate = sc;
103 break;
104 }
105
106 if (candidate is null)
107 candidate = screens[0];
108
109 PresentHostOnDedicatedScreen(hostWindow, candidate, maximizeOnDedicatedScreens);
110 }
111
112 /// <summary>
113 /// Один TopLevel на выделенном мониторе: по настройке — <see cref="WindowState.Maximized"/> или заполнение <see cref="Screen.WorkingArea"/> в DIP.
114 /// </summary>
115 private static void PresentHostOnDedicatedScreen(Window hostWindow, Screen screen, bool maximize)
116 {
117 if (maximize)
118 {
119 hostWindow.WindowState = WindowState.Normal;
120 hostWindow.Position = screen.WorkingArea.Position;
121 hostWindow.WindowState = WindowState.Maximized;
122 return;
123 }
124
125 ApplyWorkingAreaPixelsToHostWindow(hostWindow, screen, screen.WorkingArea);
126 }
127
128 /// <summary>
129 /// <see cref="Screen.WorkingArea"/> — в пикселях; <see cref="Window.Width"/>/<see cref="Window.Height"/> — в DIP (Avalonia 11).
130 /// </summary>
131 private static void ApplyWorkingAreaPixelsToHostWindow(Window hostWindow, Screen screen, PixelRect workingAreaPixels)
132 {
133 hostWindow.WindowState = WindowState.Normal;
134 var s = screen.Scaling;
135 if (s <= 0)
136 s = 1;
137 hostWindow.Position = workingAreaPixels.Position;
138 hostWindow.Width = workingAreaPixels.Width / s;
139 hostWindow.Height = workingAreaPixels.Height / s;
140 }
141
142 private static bool TryClampToWorkingAreas(
143 Window mainWindow,
144 PresentationHostWindowBounds b,
145 out PixelPoint position,
146 out double width,
147 out double height,
148 out Screen? matchedScreen)
149 {
150 position = default;
151 width = 0;
152 height = 0;
153 matchedScreen = null;
154
155 var screens = mainWindow.Screens?.All;
156 if (screens is null || screens.Count == 0)
157 return false;
158
159 var wDip = Math.Max(MinRestoreWidth, b.Width);
160 var hDip = Math.Max(MinRestoreHeight, b.Height);
161 var rect = SavedBoundsToPixelRect(mainWindow.Screens, b, wDip, hDip);
162
163 if (!TryGetScreenWithLargestOverlap(rect, screens, out var best))
164 return false;
165
166 matchedScreen = best;
167 ClampSavedPixelRectToWorkingArea(rect, best.WorkingArea, best.Scaling, out position, out width, out height);
168 return true;
169 }
170
171 /// <summary>Сохранённые Width/Height — DIP; перевод в пиксели для пересечения с <see cref="Screen.WorkingArea"/>.</summary>
172 private static PixelRect SavedBoundsToPixelRect(Screens? screens, PresentationHostWindowBounds b, double wDip, double hDip)
173 {
174 var scale = screens?.ScreenFromPoint(new PixelPoint(b.PixelX, b.PixelY))?.Scaling ?? 1.0;
175 if (scale <= 0)
176 scale = 1.0;
177 var rw = (int)Math.Round(wDip * scale);
178 var rh = (int)Math.Round(hDip * scale);
179 return new PixelRect(b.PixelX, b.PixelY, rw, rh);
180 }
181
182 private static bool TryGetScreenWithLargestOverlap(
183 PixelRect rect,
184 IReadOnlyList<Screen> screens,
185 [NotNullWhen(true)] out Screen? best)
186 {
187 best = null;
188 var bestArea = 0L;
189 foreach (var sc in screens)
190 {
191 var inter = rect.Intersect(sc.WorkingArea);
192 var area = (long)inter.Width * inter.Height;
193 if (area > bestArea)
194 {
195 bestArea = area;
196 best = sc;
197 }
198 }
199
200 return best is not null && bestArea > 0;
201 }
202
203 private static void ClampSavedPixelRectToWorkingArea(
204 PixelRect savedRect,
205 PixelRect wa,
206 double scalingRaw,
207 out PixelPoint position,
208 out double widthDip,
209 out double heightDip)
210 {
211 var scale = scalingRaw > 0 ? scalingRaw : 1.0;
212 var minWPx = (int)Math.Round(MinRestoreWidth * scale);
213 var minHPx = (int)Math.Round(MinRestoreHeight * scale);
214 var rw = savedRect.Width;
215 var rh = savedRect.Height;
216 var iw = Math.Min(rw, wa.Width);
217 var ih = Math.Min(rh, wa.Height);
218 iw = Math.Max(minWPx, iw);
219 ih = Math.Max(minHPx, ih);
220 if (iw > wa.Width)
221 iw = wa.Width;
222 if (ih > wa.Height)
223 ih = wa.Height;
224
225 var topLeft = ClampTopLeftInsideWorkingArea(savedRect.X, savedRect.Y, iw, ih, wa);
226 position = topLeft;
227 widthDip = iw / scale;
228 heightDip = ih / scale;
229 }
230
231 private static PixelPoint ClampTopLeftInsideWorkingArea(int x, int y, int iw, int ih, PixelRect wa)
232 {
233 if (x < wa.X)
234 x = wa.X;
235 if (y < wa.Y)
236 y = wa.Y;
237 if (x + iw > wa.Right)
238 x = wa.Right - iw;
239 if (y + ih > wa.Bottom)
240 y = wa.Bottom - ih;
241 if (x < wa.X)
242 x = wa.X;
243 if (y < wa.Y)
244 y = wa.Y;
245 return new PixelPoint(x, y);
246 }
247}
248
View only · write via MCP/CIDE