Skip to content

Commit 3bb9ba5

Browse files
committed
Add height splits, 4:3 all variants, download section
Features: - Height splits: zones can now snap to partial screen height (top/bottom halves, height thirds) via new HeightRatios property on GridRowDef - 4:3 defaults now include all 3 variants: LEFT (4:3), CENTER (3:4:3), RIGHT (3:4) - Default grid expanded from 5 to 9 rows including TOP/BOTTOM and HEIGHT 1/3 presets - Grid editor: new H: field for editing height ratios per row - Grid editor: new preset buttons for Top/Bottom and Height Thirds - Snap preview now correctly shows partial-height zones README: - Added prominent download section with release links - Updated default grid layout diagram for 9-row grid - Updated architecture section with test suite Tests: 54 total (was 39), all passing
1 parent 4e2204e commit 3bb9ba5

File tree

7 files changed

+360
-47
lines changed

7 files changed

+360
-47
lines changed

GridConfig.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ public class GridRowDef
2121
/// </summary>
2222
public List<int> Ratios { get; set; } = new();
2323

24+
/// <summary>
25+
/// Height ratios for vertical subdivision within each column.
26+
/// Null or single entry = full screen height (default).
27+
/// E.g. [1,1] = top/bottom halves, [1,1,1] = top/mid/bottom thirds.
28+
/// </summary>
29+
public List<int>? HeightRatios { get; set; }
30+
31+
/// <summary>Whether this row has vertical subdivisions.</summary>
32+
[JsonIgnore]
33+
public bool HasHeightSplit => HeightRatios != null && HeightRatios.Count > 1;
34+
2435
/// <summary>Creates the display label for a column (e.g. "1/3" or "4").</summary>
2536
public string GetColumnLabel(int colIndex)
2637
{
@@ -34,6 +45,28 @@ public string GetColumnLabel(int colIndex)
3445

3546
return Ratios[colIndex].ToString();
3647
}
48+
49+
/// <summary>Creates the display label for a vertical sub-row (e.g. "Top", "Mid", "Bot").</summary>
50+
public string GetHeightLabel(int vRowIndex)
51+
{
52+
if (HeightRatios == null || HeightRatios.Count <= 1)
53+
return string.Empty;
54+
55+
bool allEqual = true;
56+
for (int i = 1; i < HeightRatios.Count; i++)
57+
if (HeightRatios[i] != HeightRatios[0]) { allEqual = false; break; }
58+
59+
if (allEqual)
60+
{
61+
if (HeightRatios.Count == 2)
62+
return vRowIndex == 0 ? "Top" : "Bot";
63+
if (HeightRatios.Count == 3)
64+
return vRowIndex == 0 ? "Top" : vRowIndex == 1 ? "Mid" : "Bot";
65+
return $"V{vRowIndex + 1}/{HeightRatios.Count}";
66+
}
67+
68+
return HeightRatios[vRowIndex].ToString();
69+
}
3770
}
3871

3972
/// <summary>
@@ -85,11 +118,15 @@ public static GridConfig CreateDefault()
85118
Name = "Default",
86119
Rows = new List<GridRowDef>
87120
{
88-
new() { Name = "HALVES", Ratios = new List<int> { 1, 1 } },
89-
new() { Name = "THIRDS", Ratios = new List<int> { 1, 1, 1 } },
90-
new() { Name = "4 : 3", Ratios = new List<int> { 4, 3 } },
91-
new() { Name = "QUARTERS", Ratios = new List<int> { 1, 1, 1, 1 } },
92-
new() { Name = "FIFTHS", Ratios = new List<int> { 1, 1, 1, 1, 1 } },
121+
new() { Name = "HALVES", Ratios = new List<int> { 1, 1 } },
122+
new() { Name = "THIRDS", Ratios = new List<int> { 1, 1, 1 } },
123+
new() { Name = "4:3 LEFT", Ratios = new List<int> { 4, 3 } },
124+
new() { Name = "4:3 CENTER", Ratios = new List<int> { 3, 4, 3 } },
125+
new() { Name = "4:3 RIGHT", Ratios = new List<int> { 3, 4 } },
126+
new() { Name = "QUARTERS", Ratios = new List<int> { 1, 1, 1, 1 } },
127+
new() { Name = "FIFTHS", Ratios = new List<int> { 1, 1, 1, 1, 1 } },
128+
new() { Name = "TOP / BOTTOM", Ratios = new List<int> { 1 }, HeightRatios = new List<int> { 1, 1 } },
129+
new() { Name = "HEIGHT ⅓", Ratios = new List<int> { 1 }, HeightRatios = new List<int> { 1, 1, 1 } },
93130
}
94131
};
95132
}

GridEditorWindow.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
<Button Style="{StaticResource FlatBtn}" Content="+ 4:3" Click="OnPreset43" Margin="0,0,4,4"/>
8989
<Button Style="{StaticResource FlatBtn}" Content="+ Quarters" Click="OnPresetQuarters" Margin="0,0,4,4"/>
9090
<Button Style="{StaticResource FlatBtn}" Content="+ Fifths" Click="OnPresetFifths" Margin="0,0,4,4"/>
91+
<Button Style="{StaticResource FlatBtn}" Content="+ Top/Bottom" Click="OnPresetTopBottom" Margin="0,0,4,4"/>
92+
<Button Style="{StaticResource FlatBtn}" Content="+ Height ⅓" Click="OnPresetHeightThirds" Margin="0,0,4,4"/>
9193
<Button Style="{StaticResource FlatBtn}" Content="+ Custom…" Click="OnPresetCustom" Margin="0,0,4,4"/>
9294
</WrapPanel>
9395

GridEditorWindow.xaml.cs

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,19 @@ private void RebuildRowsUI()
8585
var ratiosPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
8686
var lblRatios = new TextBlock
8787
{
88-
Text = "Ratios:",
88+
Text = "W:",
8989
Foreground = new SolidColorBrush(Color.FromArgb(255, 160, 160, 200)),
9090
FontSize = 12,
9191
VerticalAlignment = VerticalAlignment.Center,
92-
Margin = new Thickness(0, 0, 6, 0)
92+
Margin = new Thickness(0, 0, 4, 0),
93+
ToolTip = "Width ratios (columns)"
9394
};
9495
ratiosPanel.Children.Add(lblRatios);
9596

9697
var txtRatios = new TextBox
9798
{
9899
Text = string.Join(" : ", row.Ratios),
99-
Width = 160,
100+
Width = 110,
100101
Height = 28,
101102
FontSize = 13,
102103
Background = new SolidColorBrush(Color.FromArgb(255, 42, 42, 62)),
@@ -109,6 +110,34 @@ private void RebuildRowsUI()
109110
txtRatios.LostFocus += (_, _) => ParseRatios(txtRatios, row);
110111
ratiosPanel.Children.Add(txtRatios);
111112

113+
// Height ratios edit
114+
var lblHeight = new TextBlock
115+
{
116+
Text = "H:",
117+
Foreground = new SolidColorBrush(Color.FromArgb(255, 160, 160, 200)),
118+
FontSize = 12,
119+
VerticalAlignment = VerticalAlignment.Center,
120+
Margin = new Thickness(10, 0, 4, 0),
121+
ToolTip = "Height ratios (vertical split, leave empty for full height)"
122+
};
123+
ratiosPanel.Children.Add(lblHeight);
124+
125+
var txtHeight = new TextBox
126+
{
127+
Text = row.HasHeightSplit ? string.Join(" : ", row.HeightRatios!) : "",
128+
Width = 80,
129+
Height = 28,
130+
FontSize = 13,
131+
Background = new SolidColorBrush(Color.FromArgb(255, 42, 42, 62)),
132+
Foreground = Brushes.White,
133+
BorderBrush = new SolidColorBrush(Color.FromArgb(255, 68, 68, 102)),
134+
Padding = new Thickness(5, 3, 5, 3),
135+
VerticalContentAlignment = VerticalAlignment.Center,
136+
ToolTip = "Height ratios separated by : (e.g. 1:1 for top/bottom, empty = full height)"
137+
};
138+
txtHeight.LostFocus += (_, _) => ParseHeightRatios(txtHeight, row);
139+
ratiosPanel.Children.Add(txtHeight);
140+
112141
Grid.SetColumn(ratiosPanel, 2);
113142
grid.Children.Add(ratiosPanel);
114143

@@ -206,20 +235,61 @@ private static void ParseRatios(TextBox txt, GridRowDef row)
206235
}
207236
}
208237

238+
private static void ParseHeightRatios(TextBox txt, GridRowDef row)
239+
{
240+
var text = txt.Text.Trim();
241+
if (string.IsNullOrEmpty(text))
242+
{
243+
row.HeightRatios = null;
244+
txt.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 68, 68, 102));
245+
return;
246+
}
247+
248+
var parts = text
249+
.Replace(",", ":")
250+
.Replace(" ", "")
251+
.Split(':', StringSplitOptions.RemoveEmptyEntries);
252+
253+
var parsed = new List<int>();
254+
foreach (var p in parts)
255+
{
256+
if (int.TryParse(p, out int v) && v > 0)
257+
parsed.Add(v);
258+
}
259+
260+
if (parsed.Count >= 1)
261+
{
262+
row.HeightRatios = parsed.Count == 1 ? null : parsed;
263+
txt.Text = parsed.Count == 1 ? "" : string.Join(" : ", parsed);
264+
txt.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 68, 68, 102));
265+
}
266+
else
267+
{
268+
txt.BorderBrush = Brushes.Red;
269+
}
270+
}
271+
209272
// ── Preset buttons ──────────────────────────────────────────────
210273

211-
private void AddRowDef(string name, params int[] ratios)
274+
private void AddRowDef(string name, int[]? heightRatios, params int[] ratios)
212275
{
213-
_config.Rows.Add(new GridRowDef { Name = name, Ratios = ratios.ToList() });
276+
_config.Rows.Add(new GridRowDef
277+
{
278+
Name = name,
279+
Ratios = ratios.ToList(),
280+
HeightRatios = heightRatios?.ToList()
281+
});
214282
RebuildRowsUI();
215283
}
216284

217-
private void OnAddRow(object sender, RoutedEventArgs e) => AddRowDef("Custom", 1, 1);
218-
private void OnPresetHalves(object sender, RoutedEventArgs e) => AddRowDef("HALVES", 1, 1);
219-
private void OnPresetThirds(object sender, RoutedEventArgs e) => AddRowDef("THIRDS", 1, 1, 1);
220-
private void OnPreset43(object sender, RoutedEventArgs e) => AddRowDef("4 : 3", 4, 3);
221-
private void OnPresetQuarters(object sender, RoutedEventArgs e) => AddRowDef("QUARTERS", 1, 1, 1, 1);
222-
private void OnPresetFifths(object sender, RoutedEventArgs e) => AddRowDef("FIFTHS", 1, 1, 1, 1, 1);
285+
private void OnAddRow(object sender, RoutedEventArgs e) => AddRowDef("Custom", null, 1, 1);
286+
private void OnPresetHalves(object sender, RoutedEventArgs e) => AddRowDef("HALVES", null, 1, 1);
287+
private void OnPresetThirds(object sender, RoutedEventArgs e) => AddRowDef("THIRDS", null, 1, 1, 1);
288+
private void OnPreset43(object sender, RoutedEventArgs e) => AddRowDef("4:3 LEFT", null, 4, 3);
289+
private void OnPresetQuarters(object sender, RoutedEventArgs e) => AddRowDef("QUARTERS", null, 1, 1, 1, 1);
290+
private void OnPresetFifths(object sender, RoutedEventArgs e) => AddRowDef("FIFTHS", null, 1, 1, 1, 1, 1);
291+
private void OnPresetTopBottom(object sender, RoutedEventArgs e) => AddRowDef("TOP / BOTTOM", new[] { 1, 1 }, 1);
292+
private void OnPresetHeightThirds(object sender, RoutedEventArgs e) => AddRowDef("HEIGHT \u2153", new[] { 1, 1, 1 }, 1);
223293

224294
private void OnPresetCustom(object sender, RoutedEventArgs e)
225295
{
@@ -289,7 +359,12 @@ private static GridConfig CloneConfig(GridConfig src)
289359
{
290360
var clone = new GridConfig { Name = src.Name };
291361
foreach (var row in src.Rows)
292-
clone.Rows.Add(new GridRowDef { Name = row.Name, Ratios = new List<int>(row.Ratios) });
362+
clone.Rows.Add(new GridRowDef
363+
{
364+
Name = row.Name,
365+
Ratios = new List<int>(row.Ratios),
366+
HeightRatios = row.HeightRatios != null ? new List<int>(row.HeightRatios) : null
367+
});
293368
return clone;
294369
}
295370

OverlayWindow.xaml.cs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ private void BuildZones()
199199
foreach (int r in rowDef.Ratios) totalParts += r;
200200
if (totalParts <= 0) continue;
201201

202+
// Height ratios (vertical subdivision)
203+
var heightRatios = rowDef.HasHeightSplit ? rowDef.HeightRatios! : new List<int> { 1 };
204+
int totalHeightParts = 0;
205+
foreach (int hr in heightRatios) totalHeightParts += hr;
206+
int numVRows = heightRatios.Count;
207+
202208
int numCols = rowDef.Ratios.Count;
203209
int xOffset = screenLeft;
204210

@@ -208,15 +214,37 @@ private void BuildZones()
208214
if (col == numCols - 1)
209215
w = screenLeft + screenW - xOffset;
210216

211-
_zones.Add(new GridZone
217+
// Vertical sub-rows within this column
218+
int snapYOffset = screenTop;
219+
for (int vRow = 0; vRow < numVRows; vRow++)
212220
{
213-
DisplayBounds = new Rect(xOffset + gap, dispTop + gap, w - 2 * gap, rowH - 2 * gap),
214-
SnapBounds = new Rect(xOffset, screenTop, w, screenH),
215-
Label = rowDef.GetColumnLabel(col),
216-
Row = row,
217-
Column = col,
218-
TotalColumns = numCols
219-
});
221+
int snapH = (int)((double)heightRatios[vRow] / totalHeightParts * screenH);
222+
if (vRow == numVRows - 1)
223+
snapH = screenTop + screenH - snapYOffset;
224+
225+
int dispSubH = rowH / numVRows;
226+
int dispSubTop = dispTop + vRow * dispSubH;
227+
if (vRow == numVRows - 1)
228+
dispSubH = dispTop + rowH - dispSubTop;
229+
230+
// Label: combine column label with height label
231+
string colLabel = rowDef.GetColumnLabel(col);
232+
string hLabel = rowDef.GetHeightLabel(vRow);
233+
string label = string.IsNullOrEmpty(hLabel) ? colLabel : $"{colLabel} {hLabel}";
234+
235+
_zones.Add(new GridZone
236+
{
237+
DisplayBounds = new Rect(xOffset + gap, dispSubTop + gap, w - 2 * gap, dispSubH - 2 * gap),
238+
SnapBounds = new Rect(xOffset, snapYOffset, w, snapH),
239+
Label = label,
240+
Row = row,
241+
Column = col,
242+
TotalColumns = numCols
243+
});
244+
245+
snapYOffset += snapH;
246+
}
247+
220248
xOffset += w;
221249
}
222250
}
@@ -359,13 +387,15 @@ private void UpdateSnapPreview(GridZone? zone)
359387
}
360388

361389
double x = (zone.SnapBounds.X - _workArea.Left) / _dpiScale;
390+
double y = (zone.SnapBounds.Y - _workArea.Top) / _dpiScale;
362391
double w = zone.SnapBounds.Width / _dpiScale;
392+
double h = zone.SnapBounds.Height / _dpiScale;
363393
double previewMargin = 6;
364394

365395
Canvas.SetLeft(_snapPreview, x + previewMargin);
366-
Canvas.SetTop(_snapPreview, previewMargin);
396+
Canvas.SetTop(_snapPreview, y + previewMargin);
367397
_snapPreview.Width = w - 2 * previewMargin;
368-
_snapPreview.Height = Height - 2 * previewMargin;
398+
_snapPreview.Height = h - 2 * previewMargin;
369399
_snapPreview.Visibility = Visibility.Visible;
370400

371401
// Show a clear size label centered in the preview
@@ -379,7 +409,7 @@ private void UpdateSnapPreview(GridZone? zone)
379409
double labelW = _snapLabel.DesiredSize.Width;
380410
double labelH = _snapLabel.DesiredSize.Height;
381411
Canvas.SetLeft(_snapLabel, x + (w - labelW) / 2);
382-
Canvas.SetTop(_snapLabel, (Height - labelH) / 2);
412+
Canvas.SetTop(_snapLabel, y + (h - labelH) / 2);
383413
}
384414
}
385415

0 commit comments

Comments
 (0)