From 894bd8323ea016828cf38899b39052e29baf1c5d Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:34:26 -0600 Subject: [PATCH 01/10] New enum to replace some magic numbers --- LMeter/Helpers/Enums.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/LMeter/Helpers/Enums.cs b/LMeter/Helpers/Enums.cs index f7e73c7..7e5ecc8 100644 --- a/LMeter/Helpers/Enums.cs +++ b/LMeter/Helpers/Enums.cs @@ -135,4 +135,10 @@ public enum DrawAnchor BottomLeft = 7, BottomRight = 8, } + + public enum BarSizeType + { + ConstantCount = 0, + ConstantSize = 1, + } } From a79d49b373a43d9bc97f535af616684273765f60 Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:35:03 -0600 Subject: [PATCH 02/10] Updated and new config settings for grid layout --- LMeter/Config/BarConfig.cs | 76 ++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/LMeter/Config/BarConfig.cs b/LMeter/Config/BarConfig.cs index 62b8bb3..712b4fe 100644 --- a/LMeter/Config/BarConfig.cs +++ b/LMeter/Config/BarConfig.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using System.Text.Json.Serialization; using Dalamud.Bindings.ImGui; @@ -13,10 +14,22 @@ public class BarConfig : IConfigPage private static readonly string[] _jobIconStyleOptions = ["Style 1", "Style 2"]; - public int BarHeightType = 0; + public BarSizeType BarSizeType = BarSizeType.ConstantCount; public int BarCount = 8; - public int BarGaps = 1; + + [Obsolete($"Use {nameof(BarVerticalGaps)} instead, retained for serialization backwards compatibility")] + public int BarGaps + { + get => BarVerticalGaps; + set => BarVerticalGaps = value; + } + + public int BarVerticalGaps = 1; + public int BarHorizontalGaps = 1; public float BarHeight = 25; + public float BarWidth = 150; + public int MaxRows = 0; + public int MaxColumns = 1; public bool ShowJobIcon = true; public int JobIconSizeType = 0; @@ -83,6 +96,14 @@ public class BarConfig : IConfigPage public RoundingOptions MiddleBarRounding = new(false, 10f, RoundingFlag.All); public RoundingOptions BottomBarRounding = new(false, 10f, RoundingFlag.BottomLeft); + public RoundingOptions LeftBarRounding = new(false, 10f, RoundingFlag.Left); + public RoundingOptions RightBarRounding = new(false, 10f, RoundingFlag.Right); + + public RoundingOptions TopLeftBarRounding = new(false, 10f, RoundingFlag.TopLeft); + public RoundingOptions TopRightBarRounding = new(false, 10f, RoundingFlag.TopRight); + public RoundingOptions BottomLeftBarRounding = new(false, 10f, RoundingFlag.BottomLeft); + public RoundingOptions BottomRightBarRounding = new(false, 10f, RoundingFlag.BottomRight); + public IConfigPage GetDefault() { BarConfig defaultConfig = new() @@ -104,21 +125,30 @@ public void DrawConfig(Vector2 size, float padX, float padY, bool border = true) { if (ImGui.BeginChild($"##{this.Name}", new Vector2(size.X, size.Y), border)) { - ImGui.Text("Bar Height Type"); - ImGui.RadioButton("Constant Bar Number", ref this.BarHeightType, 0); + ImGui.Text("Bar Size Type"); + ImGui.RadioButton("Constant Bar Number", ref this.BarSizeType, BarSizeType.ConstantCount); ImGui.SameLine(); - ImGui.RadioButton("Constant Bar Height", ref this.BarHeightType, 1); + ImGui.RadioButton("Constant Bar Size", ref this.BarSizeType, BarSizeType.ConstantSize); - if (this.BarHeightType == 0) + if (this.BarSizeType == BarSizeType.ConstantCount) { ImGui.DragInt("Num Bars to Display", ref this.BarCount, 1, 1, 48); } - else if (this.BarHeightType == 1) + else if (this.BarSizeType == BarSizeType.ConstantSize) { ImGui.DragFloat("Bar Height", ref this.BarHeight, .1f, 1, 100); + ImGui.DragFloat("Bar Width", ref this.BarWidth, .1f, 1, 1000); + } + + ImGui.DragInt("Maximum Columns", ref this.MaxColumns, 1, 1, 40); + ImGui.DragInt("Maximum Rows", ref this.MaxRows, 1, 0, 40); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Set to 0 for unlimited"); } - ImGui.DragInt("Bar Gap Size", ref this.BarGaps, 1, 0, 20); + ImGui.DragInt("Bar Vertical Gap Size", ref this.BarVerticalGaps, 1, 0, 20); + ImGui.DragInt("Bar Horizontal Gap Size", ref this.BarHorizontalGaps, 1, 0, 20); ImGui.NewLine(); ImGui.DragFloat("Bar Fill Height (% of Bar Height)", ref this.BarFillHeight, .1f, 0, 1f); @@ -208,15 +238,37 @@ ref this.ColumnHeaderFontId } ImGui.Checkbox("Use your name instead of 'YOU'", ref this.UseCharacterName); - if (this.BarHeightType == 0) + if (this.BarSizeType == BarSizeType.ConstantCount) { ImGui.Checkbox("Always show your own bar", ref this.AlwaysShowSelf); } ImGui.NewLine(); - DrawHelpers.DrawRoundingOptions("Top Bar Rounded Corners", 0, this.TopBarRounding); - DrawHelpers.DrawRoundingOptions("Middle Bar Rounded Corners", 0, this.MiddleBarRounding); - DrawHelpers.DrawRoundingOptions("Bottom Bar Rounded Corners", 0, this.BottomBarRounding); + + if (MaxColumns == 1) + { + DrawHelpers.DrawRoundingOptions("Top Bar Rounded Corners", 0, this.TopBarRounding); + DrawHelpers.DrawRoundingOptions("Middle Bar Rounded Corners", 0, this.MiddleBarRounding); + DrawHelpers.DrawRoundingOptions("Bottom Bar Rounded Corners", 0, this.BottomBarRounding); + } + else if (MaxRows == 1) + { + DrawHelpers.DrawRoundingOptions("Left Bar Rounded Corners", depth: 0, this.LeftBarRounding); + DrawHelpers.DrawRoundingOptions("Middle Bar Rounded Corners", depth: 0, this.MiddleBarRounding); + DrawHelpers.DrawRoundingOptions("Right Bar Rounded Corners", depth: 0, this.RightBarRounding); + } + else + { + DrawHelpers.DrawRoundingOptions("Top Left Bar Rounded Corners", 0, this.TopLeftBarRounding); + DrawHelpers.DrawRoundingOptions("Top Middle Bar Rounded Corners", 0, this.TopBarRounding); + DrawHelpers.DrawRoundingOptions("Top Right Bar Rounded Corners", 0, this.TopRightBarRounding); + DrawHelpers.DrawRoundingOptions("Middle Left Bar Rounded Corners", 0, this.LeftBarRounding); + DrawHelpers.DrawRoundingOptions("Middle Middle Bar Rounded Corners", 0, this.MiddleBarRounding); + DrawHelpers.DrawRoundingOptions("Middle Right Bar Rounded Corners", 0, this.RightBarRounding); + DrawHelpers.DrawRoundingOptions("Bottom Left Bar Rounded Corners", 0, this.BottomLeftBarRounding); + DrawHelpers.DrawRoundingOptions("Bottom Middle Bar Rounded Corners", 0, this.BottomBarRounding); + DrawHelpers.DrawRoundingOptions("Bottom Right Bar Rounded Corners", 0, this.BottomRightBarRounding); + } } ImGui.EndChild(); From b649cf2136eb7af71a43b25d563ee43766f92b8c Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:36:22 -0600 Subject: [PATCH 03/10] New function for calculating layout from bar config and new class to hold it --- LMeter/Meter/MeterWindow.cs | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index 1d19b05..d0f2997 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -450,6 +450,77 @@ private static (Vector2, Vector2) DrawFooter( return (pos.AddY(headerConfig.HeaderHeight), size.AddY(-headerConfig.HeaderHeight)); } + private class BarLayout + { + public int Bars; + + public int Rows; + public int Columns; + + public Vector2 BarSize; + + public float Margin = 0; + } + + private BarLayout CalculateBarLayout(Vector2 size, int combatantCount) + { + BarLayout layout = new(); + + if (combatantCount == 0) + { + layout.Rows = 1; + layout.Columns = 1; + layout.BarSize = size; + layout.Bars = 1; + } + else if (this.BarConfig.BarSizeType == BarSizeType.ConstantSize) + { + var barWidth = Math.Min(this.BarConfig.BarWidth, size.X); + layout.BarSize = new Vector2(barWidth, this.BarConfig.BarHeight); + + if (this.BarConfig.MaxColumns > 1) + { + barWidth += this.BarConfig.BarHorizontalGaps; + } + + layout.Columns = Math.Min(this.BarConfig.MaxColumns, (int)Math.Floor(size.X / barWidth)); + + var barHeight = this.BarConfig.BarHeight + this.BarConfig.BarVerticalGaps; + layout.Rows = this.BarConfig.MaxRows > 0 + ? Math.Min(this.BarConfig.MaxRows, (int)Math.Ceiling(size.Y / barHeight)) + : (int)Math.Ceiling(size.Y / barHeight); + + float totalY = layout.Rows * barHeight; + layout.Margin = totalY - size.Y - this.BarConfig.BarVerticalGaps; + + layout.Bars = layout.Columns * layout.Rows; + } + else + { + layout.Columns = Math.Min(combatantCount, this.BarConfig.MaxColumns); + var totalHorizontalGaps = (layout.Columns - 1) * this.BarConfig.BarHorizontalGaps; + var barWidth = (size.X - totalHorizontalGaps) / layout.Columns; + + var effectiveBarCount = Math.Min(this.BarConfig.BarCount, combatantCount); + + layout.Rows = (int)Math.Ceiling((float)effectiveBarCount / layout.Columns); + + if (this.BarConfig.MaxRows > 0 && this.BarConfig.MaxRows < layout.Rows) + { + layout.Rows = this.BarConfig.MaxRows; + } + + var totalVerticalGaps = layout.Rows * this.BarConfig.BarVerticalGaps; + var barHeight = (int)((size.Y - totalVerticalGaps) / layout.Rows); + + layout.BarSize = new Vector2(barWidth, barHeight); + + layout.Bars = Math.Min(this.BarConfig.BarCount, layout.Rows * layout.Columns); + } + + return layout; + } + private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, Vector2 size, ActEvent? actEvent) { if (actEvent?.Combatants is not null && actEvent.Combatants.Count != 0) From 1ff0b8440a527ae0d824e9b7d74fa8b85a50c61f Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:39:04 -0600 Subject: [PATCH 04/10] Refactor: Invert actEvent checks in DrawBars A later commit will add another layer of nesting in this function, so inverting this to keep the nesting down a bit. --- LMeter/Meter/MeterWindow.cs | 165 ++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index d0f2997..a011159 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -523,105 +523,106 @@ private BarLayout CalculateBarLayout(Vector2 size, int combatantCount) private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, Vector2 size, ActEvent? actEvent) { - if (actEvent?.Combatants is not null && actEvent.Combatants.Count != 0) + if (actEvent?.Combatants is null || actEvent.Combatants.Count == 0) { - // We don't want to corrupt the cache. The entire logic past this point mutates the sorted Act combatants instead of using a rendering cache - // This has the issue that some settings can't behave properly and or don't update till the following combat update/fight - List sortedCombatants = [.. this.GetSortedCombatants(actEvent, this.GeneralConfig.DataType)]; + return; + } - // add rank to the sorted combatants, with this we have the real rank of the player - int rank = 1; - foreach (var combatant in sortedCombatants) - { - combatant.Rank = rank++; - } + // We don't want to corrupt the cache. The entire logic past this point mutates the sorted Act combatants instead of using a rendering cache + // This has the issue that some settings can't behave properly and or don't update till the following combat update/fight + List sortedCombatants = [.. this.GetSortedCombatants(actEvent, this.GeneralConfig.DataType)]; - float top = sortedCombatants[0].GetValueForDataType(this.GeneralConfig.DataType); - int barCount = this.BarConfig.BarCount; - float margin = 0; - if (this.BarConfig.BarHeightType == 1) - { - float total = 0; - barCount = 0; - do - { - barCount++; - total += this.BarConfig.BarHeight + this.BarConfig.BarGaps; - } while (total <= size.Y); - margin = total - size.Y - this.BarConfig.BarGaps; - } + // add rank to the sorted combatants, with this we have the real rank of the player + int rank = 1; + foreach (var combatant in sortedCombatants) + { + combatant.Rank = rank++; + } - int currentIndex = 0; - string playerName = Singletons.Get().CharacterName ?? "YOU"; - if (sortedCombatants.Count > barCount) + float top = sortedCombatants[0].GetValueForDataType(this.GeneralConfig.DataType); + int barCount = this.BarConfig.BarCount; + float margin = 0; + if (this.BarConfig.BarHeightType == 1) + { + float total = 0; + barCount = 0; + do { - int unclampedScroll = _scrollPosition; - currentIndex = Math.Clamp(_scrollPosition, 0, sortedCombatants.Count - barCount); - _scrollPosition = currentIndex; + barCount++; + total += this.BarConfig.BarHeight + this.BarConfig.BarGaps; + } while (total <= size.Y); + margin = total - size.Y - this.BarConfig.BarGaps; + } - if (margin > 0 && _scrollPosition < unclampedScroll) - { - _scrollShift = margin; - } + int currentIndex = 0; + string playerName = Singletons.Get().CharacterName ?? "YOU"; + if (sortedCombatants.Count > barCount) + { + int unclampedScroll = _scrollPosition; + currentIndex = Math.Clamp(_scrollPosition, 0, sortedCombatants.Count - barCount); + _scrollPosition = currentIndex; - if (unclampedScroll < 0) - { - _scrollShift = 0; - } + if (margin > 0 && _scrollPosition < unclampedScroll) + { + _scrollShift = margin; + } - if (this.BarConfig.AlwaysShowSelf && this.BarConfig.BarHeightType == 0) - { - MovePlayerIntoViewableRange(sortedCombatants, _scrollPosition, playerName); - } + if (unclampedScroll < 0) + { + _scrollShift = 0; } - localPos = localPos.AddY(-_scrollShift); - int maxIndex = Math.Min(currentIndex + barCount, sortedCombatants.Count); - int startIndex = currentIndex; - for (; currentIndex < maxIndex; currentIndex++) + if (this.BarConfig.AlwaysShowSelf && this.BarConfig.BarHeightType == 0) { - Combatant combatant = sortedCombatants[currentIndex]; - float current = combatant.GetValueForDataType(this.GeneralConfig.DataType); - ConfigColor barColor = this.BarConfig.BarColor; - ConfigColor jobColor = this.BarColorsConfig.GetColor(combatant.Job); + MovePlayerIntoViewableRange(sortedCombatants, _scrollPosition, playerName); + } + } - if (this.BarConfig.UseCustomColorForSelf && combatant.OriginalName.Equals("YOU")) - { - barColor = this.BarConfig.CustomColorForSelf; - jobColor = this.BarConfig.CustomColorForSelf; - } + localPos = localPos.AddY(-_scrollShift); + int maxIndex = Math.Min(currentIndex + barCount, sortedCombatants.Count); + int startIndex = currentIndex; + for (; currentIndex < maxIndex; currentIndex++) + { + Combatant combatant = sortedCombatants[currentIndex]; + float current = combatant.GetValueForDataType(this.GeneralConfig.DataType); + ConfigColor barColor = this.BarConfig.BarColor; + ConfigColor jobColor = this.BarColorsConfig.GetColor(combatant.Job); - combatant.NameOverwrite = this.BarConfig.UseCharacterName switch - { - true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", playerName), - false when combatant.NameOverwrite is not null => null, - _ => combatant.NameOverwrite, - }; + if (this.BarConfig.UseCustomColorForSelf && combatant.OriginalName.Equals("YOU")) + { + barColor = this.BarConfig.CustomColorForSelf; + jobColor = this.BarConfig.CustomColorForSelf; + } - RoundingOptions rounding = this.BarConfig.MiddleBarRounding; - if (currentIndex == startIndex) - { - rounding = this.BarConfig.TopBarRounding; - } - else if (currentIndex == maxIndex - 1) - { - rounding = this.BarConfig.BottomBarRounding; - } + combatant.NameOverwrite = this.BarConfig.UseCharacterName switch + { + true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", playerName), + false when combatant.NameOverwrite is not null => null, + _ => combatant.NameOverwrite, + }; - localPos = this.DrawBar( - drawList, - localPos, - size, - combatant, - jobColor, - barColor, - top, - current, - rounding - ); + RoundingOptions rounding = this.BarConfig.MiddleBarRounding; + if (currentIndex == startIndex) + { + rounding = this.BarConfig.TopBarRounding; } + else if (currentIndex == maxIndex - 1) + { + rounding = this.BarConfig.BottomBarRounding; + } + + localPos = this.DrawBar( + drawList, + localPos, + size, + combatant, + jobColor, + barColor, + top, + current, + rounding + ); } - ; } private Vector2 DrawBar( From c8a1453c72f75fe8318b36059b50c788791c0a81 Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:40:39 -0600 Subject: [PATCH 05/10] Refactor DrawBar to take in the individual bar size rather than the size of all bars --- LMeter/Meter/MeterWindow.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index a011159..14b603c 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -625,10 +625,10 @@ true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", player } } - private Vector2 DrawBar( + private void DrawBar( ImDrawListPtr drawList, Vector2 localPos, - Vector2 size, + Vector2 barSize, Combatant combatant, ConfigColor jobColor, ConfigColor barColor, @@ -638,19 +638,14 @@ RoundingOptions rounding ) { BarConfig barConfig = this.BarConfig; - float barHeight = - barConfig.BarHeightType == 0 - ? (size.Y - (barConfig.BarCount - 1) * barConfig.BarGaps) / barConfig.BarCount - : barConfig.BarHeight; Vector2 barPos = localPos; - Vector2 barSize = new(size.X, barHeight); - Vector2 barFillSize = new(size.X * (current / top), barHeight * barConfig.BarFillHeight); + Vector2 barFillSize = new(barSize.X * (current / top), barSize.Y * barConfig.BarFillHeight); if (barConfig.BarFillHeight != 1f) { - barPos = barConfig.BarFillDirection == 0 ? barPos.AddY(barHeight - barFillSize.Y) : barPos; - Vector2 barBackgroundSize = new(size.X * (current / top), barHeight); + barPos = barConfig.BarFillDirection == 0 ? barPos.AddY(barSize.Y - barFillSize.Y) : barPos; + Vector2 barBackgroundSize = new(barSize.X * (current / top), barSize.Y); drawList.AddRectFilled(localPos, localPos + barBackgroundSize, barConfig.BarBackgroundColor.Base); } @@ -666,7 +661,7 @@ RoundingOptions rounding { uint jobIconId = 62000u + (uint)combatant.Job + 100u * (uint)barConfig.JobIconStyle; Vector2 jobIconPos = localPos + barConfig.JobIconOffset; - Vector2 jobIconSize = barConfig.JobIconSizeType == 0 ? Vector2.One * barHeight : barConfig.JobIconSize; + Vector2 jobIconSize = barConfig.JobIconSizeType == 0 ? Vector2.One * barSize.Y : barConfig.JobIconSize; if (barConfig.JobIconBackgroundColor.Vector.W > 0f) { Vector2 jobIconBackgroundPos = new(jobIconPos.X, localPos.Y); @@ -682,7 +677,6 @@ RoundingOptions rounding } DrawBarTexts(drawList, this.BarTextConfig.Texts, localPos, barSize, jobColor, combatant); - return localPos.AddY(barHeight + barConfig.BarGaps); } private static void DrawBarTexts( From a125fec5544ae5cd69d7bdcf53ef44385a7c2bcf Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:45:23 -0600 Subject: [PATCH 06/10] DrawBars takes layout as parameter Note: Some code in this commit references removed variables to minimize the diff here, later commits fix this --- LMeter/Meter/MeterWindow.cs | 101 +++++++++++++++++------------------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index 14b603c..ae1a534 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -521,9 +521,9 @@ private BarLayout CalculateBarLayout(Vector2 size, int combatantCount) return layout; } - private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, Vector2 size, ActEvent? actEvent) + private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, BarLayout? layout, ActEvent? actEvent) { - if (actEvent?.Combatants is null || actEvent.Combatants.Count == 0) + if (actEvent?.Combatants is null || actEvent.Combatants.Count == 0 || layout is null) { return; } @@ -540,19 +540,6 @@ private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, Vector2 size, Ac } float top = sortedCombatants[0].GetValueForDataType(this.GeneralConfig.DataType); - int barCount = this.BarConfig.BarCount; - float margin = 0; - if (this.BarConfig.BarHeightType == 1) - { - float total = 0; - barCount = 0; - do - { - barCount++; - total += this.BarConfig.BarHeight + this.BarConfig.BarGaps; - } while (total <= size.Y); - margin = total - size.Y - this.BarConfig.BarGaps; - } int currentIndex = 0; string playerName = Singletons.Get().CharacterName ?? "YOU"; @@ -579,49 +566,57 @@ private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, Vector2 size, Ac } localPos = localPos.AddY(-_scrollShift); - int maxIndex = Math.Min(currentIndex + barCount, sortedCombatants.Count); - int startIndex = currentIndex; - for (; currentIndex < maxIndex; currentIndex++) - { - Combatant combatant = sortedCombatants[currentIndex]; - float current = combatant.GetValueForDataType(this.GeneralConfig.DataType); - ConfigColor barColor = this.BarConfig.BarColor; - ConfigColor jobColor = this.BarColorsConfig.GetColor(combatant.Job); + int maxIndex = Math.Min(currentIndex + layout.Bars, sortedCombatants.Count); - if (this.BarConfig.UseCustomColorForSelf && combatant.OriginalName.Equals("YOU")) + for (int currentRow = 0; currentRow < layout.Rows && currentIndex < maxIndex; ++currentRow) + { + for (int currentColumn = 0; currentColumn < layout.Columns && currentIndex < maxIndex; ++currentColumn, ++currentIndex) { - barColor = this.BarConfig.CustomColorForSelf; - jobColor = this.BarConfig.CustomColorForSelf; - } + Combatant combatant = sortedCombatants[currentIndex]; + float current = combatant.GetValueForDataType(this.GeneralConfig.DataType); + ConfigColor barColor = this.BarConfig.BarColor; + ConfigColor jobColor = this.BarColorsConfig.GetColor(combatant.Job); - combatant.NameOverwrite = this.BarConfig.UseCharacterName switch - { - true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", playerName), - false when combatant.NameOverwrite is not null => null, - _ => combatant.NameOverwrite, - }; + if (this.BarConfig.UseCustomColorForSelf && combatant.OriginalName.Equals("YOU")) + { + barColor = this.BarConfig.CustomColorForSelf; + jobColor = this.BarConfig.CustomColorForSelf; + } - RoundingOptions rounding = this.BarConfig.MiddleBarRounding; - if (currentIndex == startIndex) - { - rounding = this.BarConfig.TopBarRounding; - } - else if (currentIndex == maxIndex - 1) - { - rounding = this.BarConfig.BottomBarRounding; - } + combatant.NameOverwrite = this.BarConfig.UseCharacterName switch + { + true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", playerName), + false when combatant.NameOverwrite is not null => null, + _ => combatant.NameOverwrite, + }; + + RoundingOptions rounding = this.BarConfig.MiddleBarRounding; + if (currentIndex == startIndex) + { + rounding = this.BarConfig.TopBarRounding; + } + else if (currentIndex == maxIndex - 1) + { + rounding = this.BarConfig.BottomBarRounding; + } + + var barPos = new Vector2( + localPos.X + currentColumn * (layout.BarSize.X + this.BarConfig.BarHorizontalGaps), + localPos.Y + currentRow * (layout.BarSize.Y + this.BarConfig.BarVerticalGaps) + ); - localPos = this.DrawBar( - drawList, - localPos, - size, - combatant, - jobColor, - barColor, - top, - current, - rounding - ); + this.DrawBar( + drawList, + barPos, + layout.BarSize, + combatant, + jobColor, + barColor, + top, + current, + rounding + ); + } } } From 3fc5246289e2bd0af2ac17c53f57f3c9f4bca347 Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:46:07 -0600 Subject: [PATCH 07/10] Move rounding determination to separate function, since grid layouts complicate things --- LMeter/Meter/MeterWindow.cs | 40 ++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index ae1a534..b4dc5f4 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -590,15 +590,7 @@ true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", player _ => combatant.NameOverwrite, }; - RoundingOptions rounding = this.BarConfig.MiddleBarRounding; - if (currentIndex == startIndex) - { - rounding = this.BarConfig.TopBarRounding; - } - else if (currentIndex == maxIndex - 1) - { - rounding = this.BarConfig.BottomBarRounding; - } + var rounding = GetRounding(layout, currentRow, currentColumn); var barPos = new Vector2( localPos.X + currentColumn * (layout.BarSize.X + this.BarConfig.BarHorizontalGaps), @@ -620,6 +612,36 @@ true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", player } } + private RoundingOptions GetRounding(BarLayout layout, int row, int column) + { + var top = row == 0; + var bottom = row == layout.Rows - 1; + var left = column == 0; + var right = column == layout.Columns - 1; + + if (layout.Columns == 1) + { + return top ? this.BarConfig.TopBarRounding : bottom ? this.BarConfig.BottomBarRounding : this.BarConfig.MiddleBarRounding; + } + + if (layout.Rows == 1) + { + return left ? this.BarConfig.LeftBarRounding : right ? this.BarConfig.RightBarRounding : this.BarConfig.MiddleBarRounding; + } + + if (top) + { + return left ? this.BarConfig.TopLeftBarRounding : right ? this.BarConfig.TopRightBarRounding : this.BarConfig.TopBarRounding; + } + + if (bottom) + { + return left ? this.BarConfig.BottomLeftBarRounding : right ? this.BarConfig.BottomRightBarRounding : this.BarConfig.BottomBarRounding; + } + + return left ? this.BarConfig.LeftBarRounding : right ? this.BarConfig.RightBarRounding : this.BarConfig.MiddleBarRounding; + } + private void DrawBar( ImDrawListPtr drawList, Vector2 localPos, From a599ff29aaf6510b4c3352e52ce43e2c7641d82e Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:46:37 -0600 Subject: [PATCH 08/10] Update scrolling code for multi-column layouts --- LMeter/Meter/MeterWindow.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index b4dc5f4..1ff25f5 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -543,15 +543,19 @@ private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, BarLayout? layou int currentIndex = 0; string playerName = Singletons.Get().CharacterName ?? "YOU"; - if (sortedCombatants.Count > barCount) + if (sortedCombatants.Count > layout.Bars) { int unclampedScroll = _scrollPosition; - currentIndex = Math.Clamp(_scrollPosition, 0, sortedCombatants.Count - barCount); - _scrollPosition = currentIndex; - if (margin > 0 && _scrollPosition < unclampedScroll) + var hiddenRowCount = (int)Math.Ceiling((float)(sortedCombatants.Count - layout.Bars) / layout.Columns); + + _scrollPosition = Math.Clamp(_scrollPosition, 0, hiddenRowCount); + + currentIndex = _scrollPosition * layout.Columns; + + if (layout.Margin > 0 && _scrollPosition < unclampedScroll) { - _scrollShift = margin; + _scrollShift = layout.Margin; } if (unclampedScroll < 0) @@ -559,7 +563,7 @@ private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, BarLayout? layou _scrollShift = 0; } - if (this.BarConfig.AlwaysShowSelf && this.BarConfig.BarHeightType == 0) + if (this.BarConfig.AlwaysShowSelf && this.BarConfig.BarSizeType == BarSizeType.ConstantCount) { MovePlayerIntoViewableRange(sortedCombatants, _scrollPosition, playerName); } From 1c535284b1c2c7bce0d10c7bf08944c498163d77 Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:48:44 -0600 Subject: [PATCH 09/10] Determine layout in Draw up front, moving size adjustments as needed --- LMeter/Meter/MeterWindow.cs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index 1ff25f5..798f22b 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -318,6 +318,23 @@ public void Draw(Vector2 pos) this.GeneralConfig.Rounding ); + BarLayout? layout = null; + + if (this.HeaderConfig.ShowFooter) + { + size = size.AddY(-this.HeaderConfig.FooterHeight); + } + + if (actEvent is not null) + { + if (this.BarConfig.ShowColumnHeader) + { + size = size.AddY(-this.BarConfig.ColumnHeaderHeight); + } + + layout = CalculateBarLayout(size, this.GetSortedCombatants(actEvent, this.GeneralConfig.DataType).Count); + } + if (this.BarConfig.ShowColumnHeader && actEvent is not null) { List columnHeaderTexts = GetColumnHeaderTexts(this.BarTextConfig.Texts, this.BarConfig); @@ -336,16 +353,11 @@ public void Draw(Vector2 pos) actEvent ); - (localPos, size) = (localPos.AddY(columnHeaderSize.Y), size.AddY(-columnHeaderSize.Y)); - } - - if (this.HeaderConfig.ShowFooter) - { - size = size.AddY(-this.HeaderConfig.FooterHeight); + localPos = localPos.AddY(columnHeaderSize.Y); } ImGui.PushClipRect(localPos, localPos + size, false); - this.DrawBars(drawList, localPos, size, actEvent); + this.DrawBars(drawList, localPos, layout, actEvent); ImGui.PopClipRect(); if (this.HeaderConfig.ShowFooter) From efe217c65c712b103303cc350a039ffb0c630d83 Mon Sep 17 00:00:00 2001 From: Trevor Chipley Date: Sun, 11 Jan 2026 16:49:31 -0600 Subject: [PATCH 10/10] Draw column headers at the top of each column --- LMeter/Meter/MeterWindow.cs | 39 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index 798f22b..ad20a2b 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -335,23 +335,30 @@ public void Draw(Vector2 pos) layout = CalculateBarLayout(size, this.GetSortedCombatants(actEvent, this.GeneralConfig.DataType).Count); } - if (this.BarConfig.ShowColumnHeader && actEvent is not null) + if (this.BarConfig.ShowColumnHeader && actEvent is not null && layout is not null) { - List columnHeaderTexts = GetColumnHeaderTexts(this.BarTextConfig.Texts, this.BarConfig); - Vector2 columnHeaderSize = new(size.X, this.BarConfig.ColumnHeaderHeight); - drawList.AddRectFilled( - localPos, - localPos + columnHeaderSize, - this.BarConfig.ColumnHeaderColor.Base - ); - DrawBarTexts( - drawList, - columnHeaderTexts, - localPos + this.BarConfig.ColumnHeaderOffset, - columnHeaderSize, - jobColor, - actEvent - ); + Vector2 columnHeaderSize = new(layout.BarSize.X, this.BarConfig.ColumnHeaderHeight); + + for (int i = 0; i < layout.Columns; i++) + { + List columnHeaderTexts = GetColumnHeaderTexts(this.BarTextConfig.Texts, this.BarConfig); + + var headerPos = localPos.AddX(i * (layout.BarSize.X + this.BarConfig.BarHorizontalGaps)); + + drawList.AddRectFilled( + headerPos, + headerPos + columnHeaderSize, + this.BarConfig.ColumnHeaderColor.Base + ); + DrawBarTexts( + drawList, + columnHeaderTexts, + headerPos + this.BarConfig.ColumnHeaderOffset, + columnHeaderSize, + jobColor, + actEvent + ); + } localPos = localPos.AddY(columnHeaderSize.Y); }