Files
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

386 lines
12 KiB
C#

using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// A control to handle output of message-by-message output panels, like the debug console and chat panel.
/// </summary>
[Virtual]
public partial class OutputPanel : Control
{
public const string StyleClassOutputPanelScrollDownButton = "outputPanelScrollDownButton";
[Dependency] private MarkupTagManager _tagManager = default!;
public const string StylePropertyStyleBox = "stylebox";
public bool ShowScrollDownButton
{
get => _showScrollDownButton;
set
{
_showScrollDownButton = value;
_updateScrollButtonVisibility();
}
}
private bool _showScrollDownButton;
private readonly RingBufferList<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
private int _totalContentHeight;
private bool _firstLine = true;
private StyleBox? _styleBoxOverride;
private VScrollBar _scrollBar;
private Button _scrollDownButton;
public bool ScrollFollowing { get; set; } = true;
private bool _invalidOnVisible;
public OutputPanel()
{
IoCManager.InjectDependencies(this);
MouseFilter = MouseFilterMode.Pass;
RectClipContent = true;
_scrollBar = new VScrollBar
{
Name = "_v_scroll",
HorizontalAlignment = HAlignment.Right
};
AddChild(_scrollBar);
AddChild(_scrollDownButton = new Button()
{
Name = "scrollLiveBtn",
StyleClasses = { StyleClassOutputPanelScrollDownButton },
VerticalAlignment = VAlignment.Bottom,
HorizontalAlignment = HAlignment.Center,
Text = String.Format("⬇ {0} ⬇", Loc.GetString("output-panel-scroll-down-button-text")),
MaxWidth = 300,
Visible = false,
});
_scrollDownButton.OnPressed += _ => ScrollToBottom();
_scrollBar.OnValueChanged += _ =>
{
_isAtBottom = _scrollBar.IsAtEnd;
_updateScrollButtonVisibility();
};
}
public int EntryCount => _entries.Count;
public StyleBox? StyleBoxOverride
{
get => _styleBoxOverride;
set
{
_styleBoxOverride = value;
InvalidateMeasure();
_invalidateEntries();
}
}
public void Clear()
{
_firstLine = true;
foreach (var entry in _entries)
{
entry.RemoveControls();
}
_entries.Clear();
_totalContentHeight = 0;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
_scrollBar.Value = 0;
}
public FormattedMessage GetMessage(Index index)
{
return new FormattedMessage(_entries[index].Message);
}
public void RemoveEntry(Index index)
{
var entry = _entries[index];
entry.RemoveControls();
_entries.RemoveAt(index.GetOffset(_entries.Count));
var font = _getFont();
_totalContentHeight -= entry.Height + font.GetLineSeparation(UIScale);
if (_entries.Count == 0)
{
Clear();
}
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
}
public void AddText(string text)
{
var msg = new FormattedMessage();
msg.AddText(text);
AddMessage(msg);
}
public void AddMessage(FormattedMessage message, Color? defaultColor = null)
{
AddMessage(message, RichTextEntry.DefaultTags, defaultColor);
}
public void AddMessage(FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null)
{
var entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries.Add(entry);
var font = _getFont();
AddNewItemHeight(font, entry);
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (_isAtBottom && ScrollFollowing)
{
_scrollBar.MoveToEnd();
}
}
public void SetMessage(Index index, FormattedMessage message, Color? defaultColor = null)
{
SetMessage(index, message, RichTextEntry.DefaultTags, defaultColor);
}
public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null)
{
var atBottom = !_scrollDownButton.Visible;
var oldEntry = _entries[index];
var font = _getFont();
_totalContentHeight -= oldEntry.Height + font.GetLineSeparation(UIScale);
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
var entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries[index] = entry;
AddNewItemHeight(font, in entry);
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (atBottom)
_scrollBar.Value = _scrollBar.MaxValue;
}
private void AddNewItemHeight(Font font, in RichTextEntry entry)
{
_totalContentHeight += entry.Height;
if (_firstLine)
{
_firstLine = false;
}
else
{
_totalContentHeight += font.GetLineSeparation(UIScale);
}
}
public void ScrollToBottom()
{
_scrollBar.MoveToEnd();
_isAtBottom = true;
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
var style = _getStyleBox();
var font = _getFont();
var lineSeparation = font.GetLineSeparation(UIScale);
style?.Draw(handle, PixelSizeBox, UIScale);
var contentBox = _getContentBox();
var entryOffset = -_scrollBar.Value;
// A stack for format tags.
// This stack contains the format tag to RETURN TO when popped off.
// So when a new color tag gets hit this stack gets the previous color pushed on.
var context = new MarkupDrawingContext(2);
foreach (ref var entry in _entries)
{
if (entryOffset + entry.Height < 0)
{
// Controls within the entry are the children of this control, which means they are drawn separately
// after this Draw call, so we have to mark them as invisible to prevent them from being drawn.
//
// An alternative option is to ensure that the control position updating logic in entry.Draw is always
// run, and then setting RectClipContent = true to use scissor box testing to handle the controls
// visibility
entry.HideControls();
entryOffset += entry.Height + lineSeparation;
continue;
}
if (entryOffset > contentBox.Height)
{
entry.HideControls();
// We know that every subsequent entry will also fail the test, but we also need to
// hide all the controls, so we cannot simply break out of the loop
continue;
}
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
entryOffset += entry.Height + lineSeparation;
}
}
protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
{
base.MouseWheel(args);
if (MathHelper.CloseToPercent(0, args.Delta.Y))
{
return;
}
_scrollBar.ValueTarget -= _getScrollSpeed() * args.Delta.Y;
}
protected override void Resized()
{
base.Resized();
var styleBoxSize = _getStyleBox()?.MinimumSize.Y ?? 0;
_scrollBar.Page = UIScale * (Height - styleBoxSize);
_updateScrollButtonVisibility();
_invalidateEntries();
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
return _getStyleBox()?.MinimumSize ?? Vector2.Zero;
}
internal void _invalidateEntries()
{
_totalContentHeight = 0;
var font = _getFont();
var sizeX = _getContentBox().Width;
foreach (ref var entry in _entries)
{
entry.Update(_tagManager, font, sizeX, UIScale);
_totalContentHeight += entry.Height + font.GetLineSeparation(UIScale);
}
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (_isAtBottom && ScrollFollowing)
{
_scrollBar.MoveToEnd();
}
}
[Pure]
private Font _getFont()
{
if (TryGetStyleProperty<Font>("font", out var font))
{
return font;
}
return UserInterfaceManager.ThemeDefaults.DefaultFont;
}
[Pure]
private StyleBox? _getStyleBox()
{
if (StyleBoxOverride != null)
{
return StyleBoxOverride;
}
TryGetStyleProperty<StyleBox>(StylePropertyStyleBox, out var box);
return box;
}
[Pure]
private float _getScrollSpeed()
{
// The scroll speed depends on the UI scale because the scroll bar is working with physical pixels.
return GetScrollSpeed(_getFont(), UIScale);
}
[Pure]
private UIBox2 _getContentBox()
{
var style = _getStyleBox();
var box = style?.GetContentBox(PixelSizeBox, UIScale) ?? PixelSizeBox;
box.Right = Math.Max(box.Left, box.Right - _scrollBar.DesiredPixelSize.X);
return box;
}
protected internal override void UIScaleChanged()
{
// If this control isn't visible, don't invalidate entries immediately.
// This saves invalidating the debug console if it's hidden,
// which is a huge boon as auto-scaling changes UI scale a lot in that scenario.
if (!VisibleInTree)
_invalidOnVisible = true;
else
_invalidateEntries();
base.UIScaleChanged();
}
protected override void StylePropertiesChanged()
{
base.StylePropertiesChanged();
// Font may have changed.
_invalidateEntries();
}
internal static float GetScrollSpeed(Font font, float scale)
{
return font.GetLineHeight(scale) * 2;
}
protected override void EnteredTree()
{
base.EnteredTree();
// Due to any number of reasons the entries may be invalidated if added when not visible in the tree.
// e.g. the control has not had its UI scale set and the messages were added, but the
// existing ones were valid when the UI scale was set.
_invalidateEntries();
}
protected override void VisibilityChanged(bool newVisible)
{
if (newVisible && _invalidOnVisible)
{
_invalidateEntries();
_invalidOnVisible = false;
}
}
private void _updateScrollButtonVisibility()
{
_scrollDownButton.Visible = ShowScrollDownButton && !_isAtBottom;
}
}
}