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(); 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, + } } diff --git a/LMeter/Meter/MeterWindow.cs b/LMeter/Meter/MeterWindow.cs index 1d19b05..ad20a2b 100644 --- a/LMeter/Meter/MeterWindow.cs +++ b/LMeter/Meter/MeterWindow.cs @@ -318,34 +318,53 @@ public void Draw(Vector2 pos) this.GeneralConfig.Rounding ); - if (this.BarConfig.ShowColumnHeader && actEvent is not null) + BarLayout? layout = null; + + if (this.HeaderConfig.ShowFooter) { - 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 - ); + size = size.AddY(-this.HeaderConfig.FooterHeight); + } + + if (actEvent is not null) + { + if (this.BarConfig.ShowColumnHeader) + { + size = size.AddY(-this.BarConfig.ColumnHeaderHeight); + } - (localPos, size) = (localPos.AddY(columnHeaderSize.Y), size.AddY(-columnHeaderSize.Y)); + layout = CalculateBarLayout(size, this.GetSortedCombatants(actEvent, this.GeneralConfig.DataType).Count); } - if (this.HeaderConfig.ShowFooter) + if (this.BarConfig.ShowColumnHeader && actEvent is not null && layout is not null) { - size = size.AddY(-this.HeaderConfig.FooterHeight); + 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); } ImGui.PushClipRect(localPos, localPos + size, false); - this.DrawBars(drawList, localPos, size, actEvent); + this.DrawBars(drawList, localPos, layout, actEvent); ImGui.PopClipRect(); if (this.HeaderConfig.ShowFooter) @@ -450,64 +469,131 @@ private static (Vector2, Vector2) DrawFooter( return (pos.AddY(headerConfig.HeaderHeight), size.AddY(-headerConfig.HeaderHeight)); } - private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, Vector2 size, ActEvent? actEvent) + private class BarLayout { - if (actEvent?.Combatants is not null && actEvent.Combatants.Count != 0) + 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) { - // 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)]; + var barWidth = Math.Min(this.BarConfig.BarWidth, size.X); + layout.BarSize = new Vector2(barWidth, this.BarConfig.BarHeight); - // add rank to the sorted combatants, with this we have the real rank of the player - int rank = 1; - foreach (var combatant in sortedCombatants) + if (this.BarConfig.MaxColumns > 1) { - combatant.Rank = rank++; + barWidth += this.BarConfig.BarHorizontalGaps; } - float top = sortedCombatants[0].GetValueForDataType(this.GeneralConfig.DataType); - int barCount = this.BarConfig.BarCount; - float margin = 0; - if (this.BarConfig.BarHeightType == 1) + 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) { - 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; + layout.Rows = this.BarConfig.MaxRows; } - 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; + var totalVerticalGaps = layout.Rows * this.BarConfig.BarVerticalGaps; + var barHeight = (int)((size.Y - totalVerticalGaps) / layout.Rows); - if (margin > 0 && _scrollPosition < unclampedScroll) - { - _scrollShift = margin; - } + layout.BarSize = new Vector2(barWidth, barHeight); - if (unclampedScroll < 0) - { - _scrollShift = 0; - } + layout.Bars = Math.Min(this.BarConfig.BarCount, layout.Rows * layout.Columns); + } - if (this.BarConfig.AlwaysShowSelf && this.BarConfig.BarHeightType == 0) - { - MovePlayerIntoViewableRange(sortedCombatants, _scrollPosition, playerName); - } + return layout; + } + + private void DrawBars(ImDrawListPtr drawList, Vector2 localPos, BarLayout? layout, ActEvent? actEvent) + { + if (actEvent?.Combatants is null || actEvent.Combatants.Count == 0 || layout is null) + { + return; + } + + // 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)]; + + // 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++; + } + + float top = sortedCombatants[0].GetValueForDataType(this.GeneralConfig.DataType); + + int currentIndex = 0; + string playerName = Singletons.Get().CharacterName ?? "YOU"; + if (sortedCombatants.Count > layout.Bars) + { + int unclampedScroll = _scrollPosition; + + 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 = layout.Margin; + } + + if (unclampedScroll < 0) + { + _scrollShift = 0; + } + + if (this.BarConfig.AlwaysShowSelf && this.BarConfig.BarSizeType == BarSizeType.ConstantCount) + { + MovePlayerIntoViewableRange(sortedCombatants, _scrollPosition, playerName); } + } + + localPos = localPos.AddY(-_scrollShift); + int maxIndex = Math.Min(currentIndex + layout.Bars, sortedCombatants.Count); - localPos = localPos.AddY(-_scrollShift); - int maxIndex = Math.Min(currentIndex + barCount, sortedCombatants.Count); - int startIndex = currentIndex; - for (; currentIndex < maxIndex; currentIndex++) + for (int currentRow = 0; currentRow < layout.Rows && currentIndex < maxIndex; ++currentRow) + { + for (int currentColumn = 0; currentColumn < layout.Columns && currentIndex < maxIndex; ++currentColumn, ++currentIndex) { Combatant combatant = sortedCombatants[currentIndex]; float current = combatant.GetValueForDataType(this.GeneralConfig.DataType); @@ -527,20 +613,17 @@ 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), + localPos.Y + currentRow * (layout.BarSize.Y + this.BarConfig.BarVerticalGaps) + ); - localPos = this.DrawBar( + this.DrawBar( drawList, - localPos, - size, + barPos, + layout.BarSize, combatant, jobColor, barColor, @@ -550,13 +633,42 @@ true when combatant.Name.Contains("YOU") => combatant.Name.Replace("YOU", player ); } } - ; } - private Vector2 DrawBar( + 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, - Vector2 size, + Vector2 barSize, Combatant combatant, ConfigColor jobColor, ConfigColor barColor, @@ -566,19 +678,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); } @@ -594,7 +701,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); @@ -610,7 +717,6 @@ RoundingOptions rounding } DrawBarTexts(drawList, this.BarTextConfig.Texts, localPos, barSize, jobColor, combatant); - return localPos.AddY(barHeight + barConfig.BarGaps); } private static void DrawBarTexts(