Skip to content

Commit

Permalink
Scroll view for productivity tech levels (#366)
Browse files Browse the repository at this point in the history
In #334 (comment) I
said I wanted to see a scrolling list for the productivity tech levels,
but I didn't realize that scroll views and tab controls didn't get
along. They get along now, and the preferences screen won't get too tall
with extra prod researches.

The first commit has a fix that should maybe have been its own PR?
Trying to change the level of the scrap mining productivity research
would toggle dark mode instead. (If you had a lot of milestones, it was
steel smelting productivity instead.)
  • Loading branch information
DaleStan authored Nov 29, 2024
2 parents a56f195 + 16430d2 commit 5266e93
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 36 deletions.
3 changes: 2 additions & 1 deletion Yafc.UI/ImGui/ImGuiLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ public sealed class OverlappingAllocations : IDisposable {
private readonly ImGui gui;
private readonly bool initialDrawState;
private readonly float initialTop;
private float maximumBottom;
internal float currentTop => gui.state.top;
internal float maximumBottom { get; private set; }

internal OverlappingAllocations(ImGui gui, bool alsoDraw) {
this.gui = gui;
Expand Down
2 changes: 1 addition & 1 deletion Yafc.UI/ImGui/ImGuiUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public static bool BuildCheckBox(this ImGui gui, string text, bool value, out bo
gui.BuildText(text, TextBlockDisplayStyle.Default(color));
}

if (gui.OnClick(gui.lastRect)) {
if (gui.enableDrawing && gui.OnClick(gui.lastRect)) {
newValue = !value;
return true;
}
Expand Down
40 changes: 23 additions & 17 deletions Yafc.UI/ImGui/ScrollArea.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public abstract class Scrollable(bool vertical, bool horizontal, bool collapsibl
/// <param name="availableHeight">Available height in parent context for the Scrollable</param>
public void Build(ImGui gui, float availableHeight, bool useBottomPadding = false) {
this.gui = gui;
if (!gui.enableDrawing) {
return;
}

var rect = gui.statePosition;
float width = rect.Width;

Expand Down Expand Up @@ -151,6 +155,10 @@ public float scrollX {
public abstract Vector2 MeasureContent(float width, ImGui gui);

public bool KeyDown(SDL.SDL_Keysym key) {
if (gui?.enableDrawing != true) {
return false;
}

bool ctrl = InputSystem.Instance.control;
bool shift = InputSystem.Instance.shift;

Expand Down Expand Up @@ -242,7 +250,8 @@ public void FocusChanged(bool focused) { }
/// <summary>Provides a builder to the Scrollable to render the contents.</summary>
public abstract class ScrollAreaBase : Scrollable {
protected ImGui contents;
protected readonly float height;
private ImGui.OverlappingAllocations? controller;
public float height { get; }

public ScrollAreaBase(float height, Padding padding, bool collapsible = false, bool vertical = true, bool horizontal = false) : base(vertical, horizontal, collapsible) {
contents = new ImGui(BuildContents, padding, clip: true);
Expand All @@ -254,7 +263,12 @@ protected override void PositionContent(ImGui gui, Rect viewport) {
contents.offset = -scroll;
}

public void Build(ImGui gui) => Build(gui, height);
public void Build(ImGui gui) {
controller?.Dispose();
// Copy enableDrawing permission from gui to contents.
controller = contents.StartOverlappingAllocations(gui.enableDrawing);
Build(gui, height);
}

protected abstract void BuildContents(ImGui gui);

Expand All @@ -271,18 +285,17 @@ public class ScrollArea(float height, GuiBuilder builder, Padding padding = defa
public void Rebuild() => RebuildContents();
}

public class VirtualScrollList<TData> : ScrollAreaBase {
private readonly Vector2 elementSize;
public class VirtualScrollList<TData>(float height, Vector2 elementSize, VirtualScrollList<TData>.Drawer drawer, Padding padding = default,
Action<int, int>? reorder = null, bool collapsible = false) : ScrollAreaBase(height, padding, collapsible) {

// When rendering the scrollable content, render 'blocks' of 4 rows at a time. (As far as I can tell, any positive value works. Shadow picked 4, so I kept that.)
private readonly int bufferRows = 4;
private const int BufferRows = 4;
// The first block of bufferRows that was rendered last time BuildContents was called. If it changes while scrolling, we need to re-render the scrollable content.
private int firstVisibleBlock;
private int elementsPerRow;
private IReadOnlyList<TData> _data = [];
private readonly int maxRowsVisible;
private readonly Drawer drawer;
private readonly int maxRowsVisible = MathUtils.Ceil(height / elementSize.Y) + BufferRows + 1;
private float _spacing;
private readonly Action<int, int>? reorder;

public float spacing {
get => _spacing;
Expand All @@ -302,14 +315,7 @@ public IReadOnlyList<TData> data {
}
}

public VirtualScrollList(float height, Vector2 elementSize, Drawer drawer, Padding padding = default, Action<int, int>? reorder = null, bool collapsible = false) : base(height, padding, collapsible) {
this.elementSize = elementSize;
maxRowsVisible = MathUtils.Ceil(height / this.elementSize.Y) + bufferRows + 1;
this.drawer = drawer;
this.reorder = reorder;
}

private int CalculateFirstBlock() => Math.Max(0, MathUtils.Floor((scrollY - contents.initialPadding.top) / (elementSize.Y * bufferRows)));
private int CalculateFirstBlock() => Math.Max(0, MathUtils.Floor((scrollY - contents.initialPadding.top) / (elementSize.Y * BufferRows)));

public override Vector2 scroll {
get => base.scroll;
Expand All @@ -333,7 +339,7 @@ protected override void BuildContents(ImGui gui) {
int rowCount = ((_data.Count - 1) / elementsPerRow) + 1;
firstVisibleBlock = CalculateFirstBlock();
// Scroll up until there are maxRowsVisible, or to the top.
int firstRow = Math.Max(0, Math.Min(firstVisibleBlock * bufferRows, rowCount - maxRowsVisible));
int firstRow = Math.Max(0, Math.Min(firstVisibleBlock * BufferRows, rowCount - maxRowsVisible));
int index = firstRow * elementsPerRow;

if (index >= _data.Count) {
Expand Down
70 changes: 66 additions & 4 deletions Yafc.UI/ImGui/TabControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ private void PerformLayout() {
rows.Reverse();
}

private PageDrawer? drawer;

/// <summary>
/// Call to draw this <see cref="TabControl"/> and its active page.
/// </summary>
Expand Down Expand Up @@ -316,12 +318,36 @@ public void Build(ImGui gui) {

using var controller = gui.StartOverlappingAllocations(false);

for (int i = 0; i < tabPages.Length; i++) {
controller.StartNextAllocatePass(i == activePage);
tabPages[i].Drawer?.Invoke(gui);
drawer = new(gui, controller, tabPages, activePage);
while (drawer.DrawNextPage()) { }
drawer = null;
#endregion
}

/// <summary>
/// Requests the tab control report its remaining available content height. As a side effect, the active tab page will pause drawing until
/// all other tabs have been drawn. It is not advisable to draw tab content taller than the height returned by this method.
/// </summary>
/// <remarks>It is possible for multiple tabs to call this method. If that happens, tabs that call this method earlier will get more accurate
/// results. That is, if Tab A calls this method, Tab B draws normally, and Tab C calls this method, Tab C will get a response based on the
/// height of Tab B, and can (but should not) further increase the content height. Tab A will then get a response based on the taller of tabs
/// B and C. Like tab C, A can (but also should not) again increase the content height. If A does, it will defeat tab C's attempt to use all
/// available vertical space.</remarks>
/// <param name="minimumHeight">The minimum height that the remaining content needs. The return value will not be smaller than this
/// parameter.</param>
/// <returns>The available content height, based on all tabs that did not call this method and any tabs that called this method after the
/// current tab.</returns>
/// <exception cref="InvalidOperationException">Thrown if this <see cref="TabControl"/> is not actively drawing tab pages.</exception>
public float GetRemainingContentHeight(float minimumHeight = 0) {
if (drawer == null) {
throw new InvalidOperationException($"{nameof(GetRemainingContentHeight)} must only be called from a {nameof(GuiBuilder)} that is currently building a {nameof(TabPage)}.");
}

#endregion
using (drawer.RememberState()) {
drawer.gui.AllocateRect(0, minimumHeight);
while (drawer.DrawNextPage()) { }
}
return drawer.GetHeight();
}

/// <summary>
Expand Down Expand Up @@ -362,6 +388,42 @@ internal static void BumpTab(TabRow sourceRow, float sourceCompression, TabRow d

public static implicit operator TabRow((int Start, int End, float Compression) value) => new TabRow(value.Start, value.End, value.Compression);
}

/// <summary>
/// Tracks the necessary details to allow <see cref="GetRemainingContentHeight"/> to start drawing a second tab while preserving the drawing
/// state of the current tab. Each call to <see cref="GetRemainingContentHeight"/> will interrupt the current tab drawer and the current
/// <c>while (drawer.DrawNextPage()) { }</c> loop and start a new loop. The new loop will drawing the remaining tabs (unless interrupted
/// itself) and <see cref="GetRemainingContentHeight"/> will return the height available for use by the calling tab drawer.
/// </summary>
private sealed class PageDrawer(ImGui gui, ImGui.OverlappingAllocations controller, TabPage[] tabPages, int activePage) {
public ImGui gui { get; } = gui;
private int i = -1;
private float height;
public bool DrawNextPage() {
if (++i >= tabPages.Length) {
return false;
}
controller.StartNextAllocatePass(i == activePage);
tabPages[i].Drawer?.Invoke(gui);

return true;
}

public float GetHeight() => height = controller.maximumBottom - gui.statePosition.Top;

public IDisposable RememberState() => new State(gui, controller, i == activePage);

/// <summary>
/// Saves and restores the current state when <see cref="GetRemainingContentHeight"/> needs to interrupt the current tab drawing.
/// </summary>
private sealed class State(ImGui gui, ImGui.OverlappingAllocations controller, bool drawing) : IDisposable {
private readonly float initialTop = controller.currentTop;
public void Dispose() {
controller.StartNextAllocatePass(drawing);
gui.AllocateRect(0, initialTop - controller.currentTop);
}
}
}
}

/// <summary>
Expand Down
42 changes: 29 additions & 13 deletions Yafc/Windows/PreferencesScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ namespace Yafc;
public class PreferencesScreen : PseudoScreen {
private static readonly PreferencesScreen Instance = new PreferencesScreen();
private const int GENERAL_PAGE = 0, PROGRESSION_PAGE = 1;
private readonly TabControl tabControl = new(("General", DrawGeneral), ("Progression", DrawProgression));
private readonly TabControl tabControl;

private PreferencesScreen() => tabControl = new(("General", DrawGeneral), ("Progression", DrawProgression));

public override void Build(ImGui gui) {
BuildHeader(gui, "Preferences");
Expand All @@ -29,7 +31,9 @@ public override void Build(ImGui gui) {
}
}

private static void DrawProgression(ImGui gui) {
private static VirtualScrollList<Technology>? technologyList;

private void DrawProgression(ImGui gui) {
ProjectPreferences preferences = Project.current.preferences;

ChooseObject(gui, "Default belt:", Database.allBelts, preferences.defaultBelt, s => {
Expand Down Expand Up @@ -75,17 +79,29 @@ private static void DrawProgression(ImGui gui) {
}
}

IEnumerable<Technology> productivityTech = Database.technologies.all
.Where(x => x.changeRecipeProductivity.Count != 0)
.OrderBy(x => x.locName);
foreach (var tech in productivityTech) {
using (gui.EnterRow()) {
gui.BuildFactorioObjectButton(tech, ButtonDisplayStyle.Default);
gui.BuildText($"{tech.locName} Level: ", topOffset: 0.5f);
int currentLevel = Project.current.settings.productivityTechnologyLevels.GetValueOrDefault(tech, 0);
if (gui.BuildIntegerInput(currentLevel, out int newLevel) && newLevel >= 0) {
Project.current.settings.RecordUndo().productivityTechnologyLevels[tech] = newLevel;
}
int count = technologyList?.data.Count ?? Database.technologies.all.Count(x => x.changeRecipeProductivity.Count != 0);
float height = tabControl.GetRemainingContentHeight(Math.Min(count, 6) * 2.75f + 0.25f);
if (technologyList?.height != height) {
technologyList = new(height, new(gui.layoutRect.Width, 2.75f), DrawTechnology) {
data = [.. Database.technologies.all
.Where(x => x.changeRecipeProductivity.Count != 0)
.OrderBy(x => x.locName)]
};
}

technologyList.Build(gui);
technologyList.RebuildContents();
}

private void DrawTechnology(ImGui gui, Technology tech, int _) {
using (gui.EnterGroup(new(0, .25f, 0, .75f)))
using (gui.EnterRow()) {
gui.allocator = RectAllocator.LeftRow;
gui.BuildFactorioObjectButton(tech, ButtonDisplayStyle.Default);
gui.BuildText($"{tech.locName} Level: ");
int currentLevel = Project.current.settings.productivityTechnologyLevels.GetValueOrDefault(tech, 0);
if (gui.BuildIntegerInput(currentLevel, out int newLevel) && newLevel >= 0) {
Project.current.settings.RecordUndo().productivityTechnologyLevels[tech] = newLevel;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Date: November 21st 2024
- Hide blueprint parameters and the synthetic I and O items from more selection windows.
Internal changes:
- Dependency and automation analysis allows more ORs, e.g. "(spawner and capture-ammo) or item-to-place".
- Scroll views can appear on tab controls.
----------------------------------------------------------------------------------------------------------------------
Version: 2.3.1
Date: November 10th 2024
Expand Down

0 comments on commit 5266e93

Please sign in to comment.