реверт чатстака на время фиксов (#947)

* Revert "a (#939)"

This reverts commit 3239865364.

* Revert "Chat Stack (#935)"

This reverts commit 79fd150bf0.
This commit is contained in:
haiwwkes 2024-12-27 20:29:14 +05:00 committed by GitHub
parent 4252733127
commit 25b439a971
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 13 additions and 1126 deletions

View file

@ -27,28 +27,6 @@ namespace Content.Client.Chat.UI
Looc
}
protected RichTextLabel? ContentLabel;
public void UpdateText(ChatMessage message, int repeat)
{
if (ContentLabel == null)
return;
if (this is TextSpeechBubble)
{
var updatedMessage = $"{message.WrappedMessage} x{repeat}";
ContentLabel.SetMessage(FormatSpeech(updatedMessage));
}
else if (this is FancyTextSpeechBubble)
{
var bubbleContent = SharedChatSystem.GetStringInsideTag(message, "BubbleContent");
var updatedMessage = $"{bubbleContent} x{repeat}";
ContentLabel.SetMessage(FormatSpeech(updatedMessage));
}
_timeLeft = TotalTime;
}
/// <summary>
/// The total time a speech bubble stays on screen.
/// </summary>
@ -228,17 +206,17 @@ namespace Content.Client.Chat.UI
protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null)
{
ContentLabel = new RichTextLabel
var label = new RichTextLabel
{
MaxWidth = SpeechMaxWidth,
};
ContentLabel.SetMessage(FormatSpeech(message.WrappedMessage, fontColor));
label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor));
var panel = new PanelContainer
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { ContentLabel },
Children = { label },
ModulateSelfOverride = Color.White.WithAlpha(0.75f)
};
@ -258,17 +236,17 @@ namespace Content.Client.Chat.UI
{
if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles))
{
ContentLabel = new RichTextLabel
var label = new RichTextLabel
{
MaxWidth = SpeechMaxWidth
};
ContentLabel.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor));
label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor));
var unfanciedPanel = new PanelContainer
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { ContentLabel },
Children = { label },
ModulateSelfOverride = Color.White.WithAlpha(0.75f)
};
return unfanciedPanel;
@ -279,7 +257,7 @@ namespace Content.Client.Chat.UI
Margin = new Thickness(1, 1, 1, 1)
};
ContentLabel = new RichTextLabel
var bubbleContent = new RichTextLabel
{
MaxWidth = SpeechMaxWidth,
Margin = new Thickness(2, 6, 2, 2),
@ -288,13 +266,13 @@ namespace Content.Client.Chat.UI
//We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr
bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor));
ContentLabel.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor));
bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor));
//As for below: Some day this could probably be converted to xaml. But that is not today. -Myr
var mainPanel = new PanelContainer
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { ContentLabel },
Children = { bubbleContent },
ModulateSelfOverride = Color.White.WithAlpha(0.75f),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Bottom,

View file

@ -45,14 +45,6 @@ using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Chat;
public class SpeechBubbleStack
{
public required SpeechBubble Bubble { get; set; }
public required string Message { get; set; }
public int RepeatCount { get; set; } = 1;
public SpeechBubble.SpeechType Type { get; set; }
}
public sealed class ChatUIController : UIController
{
[Dependency] private readonly IClientAdminManager _admin = default!;
@ -180,10 +172,6 @@ public sealed class ChatUIController : UIController
public ChatSelectChannel SelectableChannels { get; private set; }
private ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;
public ChatMessage? LastMessage = null;
private readonly Dictionary<EntityUid, SpeechBubbleStack> _lastBubbles = new();
public event Action<ChatSelectChannel>? CanSendChannelsChanged;
public event Action<ChatChannel>? FilterableChannelsChanged;
public event Action<ChatSelectChannel>? SelectableChannelsChanged;
@ -466,19 +454,9 @@ public sealed class ChatUIController : UIController
private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData)
{
var message = speechData.Message;
var type = speechData.Type;
var bubble =
SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity);
if (_lastBubbles.TryGetValue(entity, out var stack) &&
stack.Message == message.WrappedMessage &&
stack.Type == type)
{
stack.RepeatCount++;
stack.Bubble.UpdateText(message, stack.RepeatCount);
return;
}
var bubble = SpeechBubble.CreateSpeechBubble(type, message, entity);
bubble.OnDied += SpeechBubbleDied;
if (_activeSpeechBubbles.TryGetValue(entity, out var existing))
@ -498,13 +476,6 @@ public sealed class ChatUIController : UIController
existing.Add(bubble);
_speechBubbleRoot.AddChild(bubble);
_lastBubbles[entity] = new SpeechBubbleStack
{
Bubble = bubble,
Message = message.WrappedMessage,
Type = type
};
if (existing.Count > SpeechBubbleCap)
{
// Get the oldest to start fading fast.
@ -540,9 +511,6 @@ public sealed class ChatUIController : UIController
var list = _activeSpeechBubbles[entityUid];
list.Remove(bubble);
if (_lastBubbles.TryGetValue(entityUid, out var stack) && stack.Bubble == bubble)
_lastBubbles.Remove(entityUid);
if (list.Count == 0)
{
_activeSpeechBubbles.Remove(entityUid);
@ -912,22 +880,6 @@ public sealed class ChatUIController : UIController
}
}
if (LastMessage != null && msg.Message == LastMessage.Message)
{
LastMessage.repeat++;
LastMessage.WrappedMessage = msg.WrappedMessage + $" x{LastMessage.repeat}";
foreach (var chat in _chats)
{
chat.UpdateMessage(chat.GetHistoryLength() - 1, LastMessage);
}
TryBuble(msg, speechBubble);
return;
}
LastMessage = msg;
// Log all incoming chat to repopulate when filter is un-toggled
if (!msg.HideChat)
{
@ -946,11 +898,7 @@ public sealed class ChatUIController : UIController
}
}
TryBuble(msg, speechBubble);
}
private void TryBuble(ChatMessage msg, bool speechBubble)
{
// Local messages that have an entity attached get a speech bubble.
if (!speechBubble || msg.SenderEntity == default)
return;

View file

@ -2,7 +2,6 @@
xmlns="https://spacestation14.io"
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls"
xmlns:controls1="clr-namespace:Content.Client._Sunrise.UI.Controls"
MouseFilter="Stop"
HorizontalExpand="True"
VerticalExpand="True"
@ -10,7 +9,7 @@
<PanelContainer Name="ChatWindowPanel" Access="Public" HorizontalExpand="True" VerticalExpand="True"
StyleClasses="StyleNano.StyleClassChatPanel">
<BoxContainer Orientation="Vertical" SeparationOverride="4" HorizontalExpand="True" VerticalExpand="True">
<controls1:SunriseOutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" />
<OutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" />
<controls:ChatInputBox HorizontalExpand="True" Name="ChatInput" Access="Public" Margin="2"/>
</BoxContainer>
</PanelContainer>

View file

@ -104,29 +104,6 @@ public partial class ChatBox : UIWidget
}
}
public int GetHistoryLength()
{
return _controller.History.Count;
}
public void UpdateMessage(int index, ChatMessage message)
{
var updatedTuple = (_controller.History[index].Item1, message);
_controller.History.Pop();
_controller.History.Add(updatedTuple);
var color = message.MessageColorOverride != null
? message.MessageColorOverride.Value
: message.Channel.TextColor();
var formatted = new FormattedMessage(3);
formatted.AddMarkup(message.WrappedMessage);
formatted.PushColor(color);
Contents.UpdateLastMessage(formatted);
}
public void AddLine(string message, Color color)
{
var formatted = new FormattedMessage(3);

View file

@ -1,289 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Utility;
namespace Content.Client._Sunrise.UI.Controls;
[Virtual]
public sealed class SunriseOutputPanel : Control
{
[Dependency] private readonly MarkupTagManager _tagManager = default!;
public const string StylePropertyStyleBox = "stylebox";
private readonly SunriseRingBufferList<SunriseRichTextEntry> _entries = new();
private bool _isAtBottom = true;
private int _totalContentHeight;
private bool _firstLine = true;
private StyleBox? _styleBoxOverride;
private VScrollBar _scrollBar;
public bool ScrollFollowing { get; set; } = true;
private bool _invalidOnVisible;
public SunriseOutputPanel()
{
IoCManager.InjectDependencies(this);
MouseFilter = Control.MouseFilterMode.Pass;
RectClipContent = true;
_scrollBar = new VScrollBar
{
Name = "_v_scroll",
HorizontalAlignment = Control.HAlignment.Right
};
AddChild(_scrollBar);
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
}
public int EntryCount => _entries.Count;
public void UpdateLastMessage(FormattedMessage message)
{
var newEnt = new SunriseRichTextEntry(message, this, _tagManager, null);
newEnt.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries[_entries.Count - 1] = newEnt;
}
public StyleBox? StyleBoxOverride
{
get => _styleBoxOverride;
set
{
_styleBoxOverride = value;
InvalidateMeasure();
_invalidateEntries();
}
}
public void Clear()
{
_firstLine = true;
_entries.Clear();
_totalContentHeight = 0;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
_scrollBar.Value = 0;
}
public void RemoveEntry(Index index)
{
var entry = _entries[index];
_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)
{
var entry = new SunriseRichTextEntry(message, this, _tagManager, null);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries.Add(entry);
var font = _getFont();
_totalContentHeight += entry.Height;
if (_firstLine)
{
_firstLine = false;
}
else
{
_totalContentHeight += font.GetLineSeparation(UIScale);
}
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (_isAtBottom && ScrollFollowing)
{
_scrollBar.MoveToEnd();
}
}
public void ScrollToBottom()
{
_scrollBar.MoveToEnd();
_isAtBottom = true;
}
protected 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();
continue;
}
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
entryOffset += entry.Height + lineSeparation;
}
}
protected 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);
_invalidateEntries();
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
return _getStyleBox()?.MinimumSize ?? Vector2.Zero;
}
private 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();
}
}
[System.Diagnostics.Contracts.Pure]
private Font _getFont()
{
if (TryGetStyleProperty<Font>("font", out var font))
{
return font;
}
return UserInterfaceManager.ThemeDefaults.DefaultFont;
}
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getStyleBox()
{
if (StyleBoxOverride != null)
{
return StyleBoxOverride;
}
TryGetStyleProperty<StyleBox>(StylePropertyStyleBox, out var box);
return box;
}
[System.Diagnostics.Contracts.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);
}
[System.Diagnostics.Contracts.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 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();
}
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;
}
}
}

View file

@ -1,260 +0,0 @@
using System.Numerics;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.Utility;
namespace Content.Client._Sunrise.UI.Controls;
internal struct SunriseRichTextEntry
{
private readonly Color _defaultColor;
private readonly Type[]? _tagsAllowed;
public readonly FormattedMessage Message;
/// <summary>
/// The vertical size of this entry, in pixels.
/// </summary>
public int Height;
/// <summary>
/// The horizontal size of this entry, in pixels.
/// </summary>
public int Width;
/// <summary>
/// The combined text indices in the message's text tags to put line breaks.
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control>? _tagControls;
public SunriseRichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
Message = message;
Height = 0;
Width = 0;
LineBreaks = default;
_defaultColor = defaultColor ?? new(200, 200, 200);
_tagsAllowed = tagsAllowed;
Dictionary<int, Control>? tagControls = null;
var nodeIndex = -1;
foreach (var node in Message)
{
nodeIndex++;
if (node.Name == null)
continue;
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
continue;
parent.Children.Add(control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
}
/// <summary>
/// Recalculate line dimensions and where it has line breaks for word wrapping.
/// </summary>
/// <param name="defaultFont">The font being used for display.</param>
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
// I am so deeply sorry for the person adding stuff to this in the future.
Height = defaultFont.GetHeight(uiScale);
LineBreaks.Clear();
int? breakLine;
var wordWrap = new SunriseWordWrap(maxSizeX);
var context = new MarkupDrawingContext();
context.Font.Push(defaultFont);
context.Color.Push(_defaultColor);
// Go over every node.
// Nodes can change the markup drawing context and return additional text.
// It's also possible for nodes to return inline controls. They get treated as one large rune.
var nodeIndex = -1;
foreach (var node in Message)
{
nodeIndex++;
var text = ProcessNode(tagManager, node, context);
if (!context.Font.TryPeek(out var font))
font = defaultFont;
// And go over every character.
foreach (var rune in text.EnumerateRunes())
{
if (ProcessRune(ref this, rune, out breakLine))
continue;
// Uh just skip unknown characters I guess.
if (!font.TryGetCharMetrics(rune, uiScale, out var metrics))
continue;
if (ProcessMetric(ref this, metrics, out breakLine))
return;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
control.Measure(new Vector2(Width, Height));
var desiredSize = control.DesiredPixelSize;
var controlMetrics = new CharMetrics(
0, 0,
desiredSize.X,
desiredSize.X,
desiredSize.Y);
if (ProcessMetric(ref this, controlMetrics, out breakLine))
return;
}
Width = wordWrap.FinalizeText(out breakLine);
CheckLineBreak(ref this, breakLine);
bool ProcessRune(ref SunriseRichTextEntry src, Rune rune, out int? outBreakLine)
{
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
CheckLineBreak(ref src, breakLine);
CheckLineBreak(ref src, breakNewLine);
outBreakLine = breakLine;
return skip;
}
bool ProcessMetric(ref SunriseRichTextEntry src, CharMetrics metrics, out int? outBreakLine)
{
wordWrap.NextMetrics(metrics, out breakLine, out var abort);
CheckLineBreak(ref src, breakLine);
outBreakLine = breakLine;
return abort;
}
void CheckLineBreak(ref SunriseRichTextEntry src, int? line)
{
if (line is { } l)
{
src.LineBreaks.Add(l);
if (!context.Font.TryPeek(out var font))
font = defaultFont;
src.Height += GetLineHeight(font, uiScale, lineHeightScale);
}
}
}
internal readonly void HideControls()
{
if (_tagControls == null)
return;
foreach (var control in _tagControls.Values)
{
control.Visible = false;
}
}
public readonly void Draw(
MarkupTagManager tagManager,
DrawingHandleBase handle,
Font defaultFont,
UIBox2 drawBox,
float verticalOffset,
MarkupDrawingContext context,
float uiScale,
float lineHeightScale = 1)
{
context.Clear();
context.Color.Push(_defaultColor);
context.Font.Push(defaultFont);
var globalBreakCounter = 0;
var lineBreakIndex = 0;
var baseLine = drawBox.TopLeft + new Vector2(0, defaultFont.GetAscent(uiScale) + verticalOffset);
var controlYAdvance = 0f;
var nodeIndex = -1;
foreach (var node in Message)
{
nodeIndex++;
var text = ProcessNode(tagManager, node, context);
if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font))
{
color = _defaultColor;
font = defaultFont;
}
foreach (var rune in text.EnumerateRunes())
{
if (lineBreakIndex < LineBreaks.Count &&
LineBreaks[lineBreakIndex] == globalBreakCounter)
{
baseLine = new Vector2(drawBox.Left, baseLine.Y + GetLineHeight(font, uiScale, lineHeightScale) + controlYAdvance);
controlYAdvance = 0;
lineBreakIndex += 1;
}
var advance = font.DrawChar(handle, rune, baseLine, uiScale, color);
baseLine += new Vector2(advance, 0);
globalBreakCounter += 1;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
// Controls may have been previously hidden via HideControls due to being "out-of frame".
// If this ever gets replaced with RectClipContents / scissor box testing, this can be removed.
control.Visible = true;
var invertedScale = 1f / uiScale;
var pos = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale);
LayoutContainer.SetPosition(control, pos);
control.Measure(new Vector2(Width, Height));
var advanceX = control.DesiredPixelSize.X;
controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale);
baseLine += new Vector2(advanceX, 0);
}
}
private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context)
{
// If a nodes name is null it's a text node.
if (node.Name == null)
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)
{
tag.PushDrawContext(node, context);
return tag.TextBefore(node);
}
tag.PopDrawContext(node, context);
return tag.TextAfter(node);
}
private static int GetLineHeight(Font font, float uiScale, float lineHeightScale)
{
var height = font.GetLineHeight(uiScale);
return (int)(height * lineHeightScale);
}
}

View file

@ -1,295 +0,0 @@
using System.Collections;
using System.Runtime.CompilerServices;
using Robust.Shared.Utility;
namespace Content.Client._Sunrise.UI.Controls;
public sealed class SunriseRingBufferList<T> : IList<T>
{
private T[] _items;
private int _read;
private int _write;
public SunriseRingBufferList(int capacity)
{
_items = new T[capacity];
}
public SunriseRingBufferList()
{
_items = [];
}
public int Capacity => _items.Length;
private bool IsFull => _items.Length == 0 || NextIndex(_write) == _read;
public void Add(T item)
{
if (IsFull)
Expand();
DebugTools.Assert(!IsFull);
_items[_write] = item;
_write = NextIndex(_write);
}
public void Clear()
{
_read = 0;
_write = 0;
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
Array.Clear(_items);
}
public bool Contains(T item)
{
return IndexOf(item) >= 0;
}
public void CopyTo(T[] array, int arrayIndex)
{
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
CopyTo(array.AsSpan(arrayIndex));
}
private void CopyTo(Span<T> dest)
{
if (dest.Length < Count)
throw new ArgumentException("Not enough elements in destination!");
var i = 0;
foreach (var item in this)
{
dest[i++] = item;
}
}
public bool Remove(T item)
{
var index = IndexOf(item);
if (index < 0)
return false;
RemoveAt(index);
return true;
}
public int Count
{
get
{
var length = _write - _read;
if (length >= 0)
return length;
return length + _items.Length;
}
}
public bool IsReadOnly => false;
public int IndexOf(T item)
{
var i = 0;
foreach (var containedItem in this)
{
if (EqualityComparer<T>.Default.Equals(item, containedItem))
return i;
i += 1;
}
return -1;
}
public void Insert(int index, T item)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
var length = Count;
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, length);
if (index == 0)
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
_items[_read] = default!;
_read = NextIndex(_read);
}
else if (index == length - 1)
{
_write = WrapInv(_write - 1);
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
_items[_write] = default!;
}
else
{
// If past me had better foresight I wouldn't be spending so much effort writing this right now.
var realIdx = RealIndex(index);
var origValue = _items[realIdx];
T result;
if (realIdx < _read)
{
// Scenario one: to-remove index is after break.
// One shift is needed.
// v
// X X X O X X
// W R
DebugTools.Assert(_write < _read);
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
}
else if (_write < _read)
{
// Scenario two: to-remove index is before break, but write is after.
// Two shifts are needed.
// v
// X O X X X X
// W R
var fromEnd = ShiftDown(_items.AsSpan(0, _write), default!);
result = ShiftDown(_items.AsSpan(realIdx), fromEnd);
}
else
{
// Scenario two: array is contiguous.
// One shift is needed.
// v
// X X X X O O
// R W
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
}
// Just make sure we didn't bulldozer something.
DebugTools.Assert(EqualityComparer<T>.Default.Equals(origValue, result));
_write = WrapInv(_write - 1);
}
}
private static T ShiftDown(Span<T> span, T substitution)
{
if (span.Length == 0)
return substitution;
var first = span[0];
span[1..].CopyTo(span[..^1]);
span[^1] = substitution!;
return first;
}
private T GetSlot(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
return _items[RealIndex(index)];
}
public T this[int index]
{
get => GetSlot(index);
set => _items[RealIndex(index)] = value;
}
private int RealIndex(int index)
{
return Wrap(index + _read);
}
private int NextIndex(int index) => Wrap(index + 1);
private int Wrap(int index)
{
if (index >= _items.Length)
index -= _items.Length;
return index;
}
private int WrapInv(int index)
{
if (index < 0)
index = _items.Length - 1;
return index;
}
private void Expand()
{
var prevSize = _items.Length;
var newSize = Math.Max(4, prevSize * 2);
Array.Resize(ref _items, newSize);
if (_write >= _read)
return;
// Write is behind read pointer, so we need to copy the items to be after the read pointer.
var toCopy = _items.AsSpan(0, _write);
var copyDest = _items.AsSpan(prevSize);
toCopy.CopyTo(copyDest);
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
toCopy.Clear();
_write += prevSize;
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator<T>
{
private readonly SunriseRingBufferList<T> _ringBufferList;
private int _readPos;
internal Enumerator(SunriseRingBufferList<T> ringBufferList)
{
_ringBufferList = ringBufferList;
_readPos = _ringBufferList._read - 1;
}
public bool MoveNext()
{
_readPos = _ringBufferList.NextIndex(_readPos);
return _readPos != _ringBufferList._write;
}
public void Reset()
{
this = new Enumerator(_ringBufferList);
}
public ref T Current => ref _ringBufferList._items[_readPos];
T IEnumerator<T>.Current => Current;
object? IEnumerator.Current => Current;
void IDisposable.Dispose()
{
}
}
}

View file

@ -1,170 +0,0 @@
using System.Diagnostics.Contracts;
using System.Text;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
namespace Content.Client._Sunrise.UI.Controls;
internal struct SunriseWordWrap
{
private readonly float _maxSizeX;
public float MaxUsedWidth;
// Index we put into the LineBreaks list when a line break should occur.
public int BreakIndexCounter;
public int NextBreakIndexCounter;
// If the CURRENT processing word ends up too long, this is the index to put a line break.
public (int index, float lineSize)? WordStartBreakIndex;
// Word size in pixels.
public int WordSizePixels;
// The horizontal position of the text cursor.
public int PosX;
public Rune LastRune;
// If a word is larger than maxSizeX, we split it.
// We need to keep track of some data to split it into two words.
public (int breakIndex, int wordSizePixels)? ForceSplitData = null;
public SunriseWordWrap(float maxSizeX)
{
this = default;
_maxSizeX = maxSizeX;
LastRune = new Rune('A');
}
public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip)
{
BreakIndexCounter = NextBreakIndexCounter;
NextBreakIndexCounter += rune.Utf16SequenceLength;
breakLine = null;
breakNewLine = null;
skip = false;
if (IsWordBoundary(LastRune, rune) || rune == new Rune('\n'))
{
// Word boundary means we know where the word ends.
if (PosX > _maxSizeX && LastRune != new Rune(' '))
{
DebugTools.Assert(WordStartBreakIndex.HasValue,
"wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line.");
//Ensure the assert had a chance to run and then just return
if (!WordStartBreakIndex.HasValue)
return;
// We ran into a word boundary and the word is too big to fit the previous line.
// So we insert the line break BEFORE the last word.
breakLine = WordStartBreakIndex!.Value.index;
MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize);
PosX = WordSizePixels;
}
// Start a new word since we hit a word boundary.
//wordSize = 0;
WordSizePixels = 0;
WordStartBreakIndex = (BreakIndexCounter, PosX);
ForceSplitData = null;
// Just manually handle newlines.
if (rune == new Rune('\n'))
{
MaxUsedWidth = Math.Max(MaxUsedWidth, PosX);
PosX = 0;
WordStartBreakIndex = null;
skip = true;
breakNewLine = BreakIndexCounter;
}
}
LastRune = rune;
}
public void NextMetrics(in CharMetrics metrics, out int? breakLine, out bool abort)
{
abort = false;
breakLine = null;
// Increase word size and such with the current character.
var oldWordSizePixels = WordSizePixels;
WordSizePixels += metrics.Advance;
// TODO: Theoretically, does it make sense to break after the glyph's width instead of its advance?
// It might result in some more tight packing but I doubt it'd be noticeable.
// Also definitely even more complex to implement.
PosX += metrics.Advance;
if (PosX <= _maxSizeX)
return;
if (!ForceSplitData.HasValue)
{
ForceSplitData = (BreakIndexCounter, oldWordSizePixels);
}
// Oh hey we get to break a word that doesn't fit on a single line.
if (WordSizePixels > _maxSizeX)
{
var (breakIndex, splitWordSize) = ForceSplitData.Value;
if (splitWordSize == 0)
{
// Happens if there's literally not enough space for a single character so uh...
// Yeah just don't.
abort = true;
return;
}
// Reset forceSplitData so that we can split again if necessary.
ForceSplitData = null;
breakLine = breakIndex;
WordSizePixels -= splitWordSize;
WordStartBreakIndex = null;
MaxUsedWidth = Math.Max(MaxUsedWidth, _maxSizeX);
PosX = WordSizePixels;
}
}
public int FinalizeText(out int? breakLine)
{
// This needs to happen because word wrapping doesn't get checked for the last word.
if (PosX > _maxSizeX)
{
if (!WordStartBreakIndex.HasValue)
{
Logger.Error(
"Assert fail inside RichTextEntry.Update, " +
"wordStartBreakIndex is null on method end w/ word wrap required. " +
"Dumping relevant stuff. Send this to PJB.");
// Logger.Error($"Message: {Message}");
Logger.Error($"maxSizeX: {_maxSizeX}");
Logger.Error($"maxUsedWidth: {MaxUsedWidth}");
Logger.Error($"breakIndexCounter: {BreakIndexCounter}");
Logger.Error("wordStartBreakIndex: null (duh)");
Logger.Error($"wordSizePixels: {WordSizePixels}");
Logger.Error($"posX: {PosX}");
Logger.Error($"lastChar: {LastRune}");
Logger.Error($"forceSplitData: {ForceSplitData}");
// Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
throw new Exception(
"wordStartBreakIndex can only be null if the word begins at a new line," +
"in which case this branch shouldn't be reached as" +
"the word would be split due to being longer than a single line.");
}
breakLine = WordStartBreakIndex.Value.index;
MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize);
}
else
{
breakLine = null;
MaxUsedWidth = Math.Max(MaxUsedWidth, PosX);
}
return (int)MaxUsedWidth;
}
[Pure]
private static bool IsWordBoundary(Rune a, Rune b)
{
return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-');
}
}

View file

@ -37,7 +37,6 @@ namespace Content.Shared.Chat
public Color? MessageColorOverride;
public string? AudioPath;
public float AudioVolume;
public int repeat = 1;
[NonSerialized]
public bool Read;