Skip to content

Commit 5266e93

Browse files
authored
Scroll view for productivity tech levels (#366)
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.)
2 parents a56f195 + 16430d2 commit 5266e93

File tree

6 files changed

+122
-36
lines changed

6 files changed

+122
-36
lines changed

Yafc.UI/ImGui/ImGuiLayout.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ public sealed class OverlappingAllocations : IDisposable {
267267
private readonly ImGui gui;
268268
private readonly bool initialDrawState;
269269
private readonly float initialTop;
270-
private float maximumBottom;
270+
internal float currentTop => gui.state.top;
271+
internal float maximumBottom { get; private set; }
271272

272273
internal OverlappingAllocations(ImGui gui, bool alsoDraw) {
273274
this.gui = gui;

Yafc.UI/ImGui/ImGuiUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ public static bool BuildCheckBox(this ImGui gui, string text, bool value, out bo
229229
gui.BuildText(text, TextBlockDisplayStyle.Default(color));
230230
}
231231

232-
if (gui.OnClick(gui.lastRect)) {
232+
if (gui.enableDrawing && gui.OnClick(gui.lastRect)) {
233233
newValue = !value;
234234
return true;
235235
}

Yafc.UI/ImGui/ScrollArea.cs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public abstract class Scrollable(bool vertical, bool horizontal, bool collapsibl
2727
/// <param name="availableHeight">Available height in parent context for the Scrollable</param>
2828
public void Build(ImGui gui, float availableHeight, bool useBottomPadding = false) {
2929
this.gui = gui;
30+
if (!gui.enableDrawing) {
31+
return;
32+
}
33+
3034
var rect = gui.statePosition;
3135
float width = rect.Width;
3236

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

153157
public bool KeyDown(SDL.SDL_Keysym key) {
158+
if (gui?.enableDrawing != true) {
159+
return false;
160+
}
161+
154162
bool ctrl = InputSystem.Instance.control;
155163
bool shift = InputSystem.Instance.shift;
156164

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

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

257-
public void Build(ImGui gui) => Build(gui, height);
266+
public void Build(ImGui gui) {
267+
controller?.Dispose();
268+
// Copy enableDrawing permission from gui to contents.
269+
controller = contents.StartOverlappingAllocations(gui.enableDrawing);
270+
Build(gui, height);
271+
}
258272

259273
protected abstract void BuildContents(ImGui gui);
260274

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

274-
public class VirtualScrollList<TData> : ScrollAreaBase {
275-
private readonly Vector2 elementSize;
288+
public class VirtualScrollList<TData>(float height, Vector2 elementSize, VirtualScrollList<TData>.Drawer drawer, Padding padding = default,
289+
Action<int, int>? reorder = null, bool collapsible = false) : ScrollAreaBase(height, padding, collapsible) {
290+
276291
// 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.)
277-
private readonly int bufferRows = 4;
292+
private const int BufferRows = 4;
278293
// 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.
279294
private int firstVisibleBlock;
280295
private int elementsPerRow;
281296
private IReadOnlyList<TData> _data = [];
282-
private readonly int maxRowsVisible;
283-
private readonly Drawer drawer;
297+
private readonly int maxRowsVisible = MathUtils.Ceil(height / elementSize.Y) + BufferRows + 1;
284298
private float _spacing;
285-
private readonly Action<int, int>? reorder;
286299

287300
public float spacing {
288301
get => _spacing;
@@ -302,14 +315,7 @@ public IReadOnlyList<TData> data {
302315
}
303316
}
304317

305-
public VirtualScrollList(float height, Vector2 elementSize, Drawer drawer, Padding padding = default, Action<int, int>? reorder = null, bool collapsible = false) : base(height, padding, collapsible) {
306-
this.elementSize = elementSize;
307-
maxRowsVisible = MathUtils.Ceil(height / this.elementSize.Y) + bufferRows + 1;
308-
this.drawer = drawer;
309-
this.reorder = reorder;
310-
}
311-
312-
private int CalculateFirstBlock() => Math.Max(0, MathUtils.Floor((scrollY - contents.initialPadding.top) / (elementSize.Y * bufferRows)));
318+
private int CalculateFirstBlock() => Math.Max(0, MathUtils.Floor((scrollY - contents.initialPadding.top) / (elementSize.Y * BufferRows)));
313319

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

339345
if (index >= _data.Count) {

Yafc.UI/ImGui/TabControl.cs

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ private void PerformLayout() {
239239
rows.Reverse();
240240
}
241241

242+
private PageDrawer? drawer;
243+
242244
/// <summary>
243245
/// Call to draw this <see cref="TabControl"/> and its active page.
244246
/// </summary>
@@ -316,12 +318,36 @@ public void Build(ImGui gui) {
316318

317319
using var controller = gui.StartOverlappingAllocations(false);
318320

319-
for (int i = 0; i < tabPages.Length; i++) {
320-
controller.StartNextAllocatePass(i == activePage);
321-
tabPages[i].Drawer?.Invoke(gui);
321+
drawer = new(gui, controller, tabPages, activePage);
322+
while (drawer.DrawNextPage()) { }
323+
drawer = null;
324+
#endregion
325+
}
326+
327+
/// <summary>
328+
/// Requests the tab control report its remaining available content height. As a side effect, the active tab page will pause drawing until
329+
/// all other tabs have been drawn. It is not advisable to draw tab content taller than the height returned by this method.
330+
/// </summary>
331+
/// <remarks>It is possible for multiple tabs to call this method. If that happens, tabs that call this method earlier will get more accurate
332+
/// 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
333+
/// 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
334+
/// 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
335+
/// available vertical space.</remarks>
336+
/// <param name="minimumHeight">The minimum height that the remaining content needs. The return value will not be smaller than this
337+
/// parameter.</param>
338+
/// <returns>The available content height, based on all tabs that did not call this method and any tabs that called this method after the
339+
/// current tab.</returns>
340+
/// <exception cref="InvalidOperationException">Thrown if this <see cref="TabControl"/> is not actively drawing tab pages.</exception>
341+
public float GetRemainingContentHeight(float minimumHeight = 0) {
342+
if (drawer == null) {
343+
throw new InvalidOperationException($"{nameof(GetRemainingContentHeight)} must only be called from a {nameof(GuiBuilder)} that is currently building a {nameof(TabPage)}.");
322344
}
323345

324-
#endregion
346+
using (drawer.RememberState()) {
347+
drawer.gui.AllocateRect(0, minimumHeight);
348+
while (drawer.DrawNextPage()) { }
349+
}
350+
return drawer.GetHeight();
325351
}
326352

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

363389
public static implicit operator TabRow((int Start, int End, float Compression) value) => new TabRow(value.Start, value.End, value.Compression);
364390
}
391+
392+
/// <summary>
393+
/// Tracks the necessary details to allow <see cref="GetRemainingContentHeight"/> to start drawing a second tab while preserving the drawing
394+
/// state of the current tab. Each call to <see cref="GetRemainingContentHeight"/> will interrupt the current tab drawer and the current
395+
/// <c>while (drawer.DrawNextPage()) { }</c> loop and start a new loop. The new loop will drawing the remaining tabs (unless interrupted
396+
/// itself) and <see cref="GetRemainingContentHeight"/> will return the height available for use by the calling tab drawer.
397+
/// </summary>
398+
private sealed class PageDrawer(ImGui gui, ImGui.OverlappingAllocations controller, TabPage[] tabPages, int activePage) {
399+
public ImGui gui { get; } = gui;
400+
private int i = -1;
401+
private float height;
402+
public bool DrawNextPage() {
403+
if (++i >= tabPages.Length) {
404+
return false;
405+
}
406+
controller.StartNextAllocatePass(i == activePage);
407+
tabPages[i].Drawer?.Invoke(gui);
408+
409+
return true;
410+
}
411+
412+
public float GetHeight() => height = controller.maximumBottom - gui.statePosition.Top;
413+
414+
public IDisposable RememberState() => new State(gui, controller, i == activePage);
415+
416+
/// <summary>
417+
/// Saves and restores the current state when <see cref="GetRemainingContentHeight"/> needs to interrupt the current tab drawing.
418+
/// </summary>
419+
private sealed class State(ImGui gui, ImGui.OverlappingAllocations controller, bool drawing) : IDisposable {
420+
private readonly float initialTop = controller.currentTop;
421+
public void Dispose() {
422+
controller.StartNextAllocatePass(drawing);
423+
gui.AllocateRect(0, initialTop - controller.currentTop);
424+
}
425+
}
426+
}
365427
}
366428

367429
/// <summary>

Yafc/Windows/PreferencesScreen.cs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ namespace Yafc;
99
public class PreferencesScreen : PseudoScreen {
1010
private static readonly PreferencesScreen Instance = new PreferencesScreen();
1111
private const int GENERAL_PAGE = 0, PROGRESSION_PAGE = 1;
12-
private readonly TabControl tabControl = new(("General", DrawGeneral), ("Progression", DrawProgression));
12+
private readonly TabControl tabControl;
13+
14+
private PreferencesScreen() => tabControl = new(("General", DrawGeneral), ("Progression", DrawProgression));
1315

1416
public override void Build(ImGui gui) {
1517
BuildHeader(gui, "Preferences");
@@ -29,7 +31,9 @@ public override void Build(ImGui gui) {
2931
}
3032
}
3133

32-
private static void DrawProgression(ImGui gui) {
34+
private static VirtualScrollList<Technology>? technologyList;
35+
36+
private void DrawProgression(ImGui gui) {
3337
ProjectPreferences preferences = Project.current.preferences;
3438

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

78-
IEnumerable<Technology> productivityTech = Database.technologies.all
79-
.Where(x => x.changeRecipeProductivity.Count != 0)
80-
.OrderBy(x => x.locName);
81-
foreach (var tech in productivityTech) {
82-
using (gui.EnterRow()) {
83-
gui.BuildFactorioObjectButton(tech, ButtonDisplayStyle.Default);
84-
gui.BuildText($"{tech.locName} Level: ", topOffset: 0.5f);
85-
int currentLevel = Project.current.settings.productivityTechnologyLevels.GetValueOrDefault(tech, 0);
86-
if (gui.BuildIntegerInput(currentLevel, out int newLevel) && newLevel >= 0) {
87-
Project.current.settings.RecordUndo().productivityTechnologyLevels[tech] = newLevel;
88-
}
82+
int count = technologyList?.data.Count ?? Database.technologies.all.Count(x => x.changeRecipeProductivity.Count != 0);
83+
float height = tabControl.GetRemainingContentHeight(Math.Min(count, 6) * 2.75f + 0.25f);
84+
if (technologyList?.height != height) {
85+
technologyList = new(height, new(gui.layoutRect.Width, 2.75f), DrawTechnology) {
86+
data = [.. Database.technologies.all
87+
.Where(x => x.changeRecipeProductivity.Count != 0)
88+
.OrderBy(x => x.locName)]
89+
};
90+
}
91+
92+
technologyList.Build(gui);
93+
technologyList.RebuildContents();
94+
}
95+
96+
private void DrawTechnology(ImGui gui, Technology tech, int _) {
97+
using (gui.EnterGroup(new(0, .25f, 0, .75f)))
98+
using (gui.EnterRow()) {
99+
gui.allocator = RectAllocator.LeftRow;
100+
gui.BuildFactorioObjectButton(tech, ButtonDisplayStyle.Default);
101+
gui.BuildText($"{tech.locName} Level: ");
102+
int currentLevel = Project.current.settings.productivityTechnologyLevels.GetValueOrDefault(tech, 0);
103+
if (gui.BuildIntegerInput(currentLevel, out int newLevel) && newLevel >= 0) {
104+
Project.current.settings.RecordUndo().productivityTechnologyLevels[tech] = newLevel;
89105
}
90106
}
91107
}

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Date: November 21st 2024
3232
- Hide blueprint parameters and the synthetic I and O items from more selection windows.
3333
Internal changes:
3434
- Dependency and automation analysis allows more ORs, e.g. "(spawner and capture-ammo) or item-to-place".
35+
- Scroll views can appear on tab controls.
3536
----------------------------------------------------------------------------------------------------------------------
3637
Version: 2.3.1
3738
Date: November 10th 2024

0 commit comments

Comments
 (0)