Skip to content

Commit

Permalink
Chat Stack (#935)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhailrake authored Dec 26, 2024
1 parent cf85950 commit 79fd150
Show file tree
Hide file tree
Showing 8 changed files with 1,057 additions and 1 deletion.
17 changes: 17 additions & 0 deletions Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbl
public ChatSelectChannel SelectableChannels { get; private set; }
private ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;

public ChatMessage? LastMessage = null;

public event Action<ChatSelectChannel>? CanSendChannelsChanged;
public event Action<ChatChannel>? FilterableChannelsChanged;
public event Action<ChatSelectChannel>? SelectableChannelsChanged;
Expand Down Expand Up @@ -880,6 +882,21 @@ public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
}
}

if (LastMessage != null && msg.Message == LastMessage.Message)
{
LastMessage.repeat++;
LastMessage.WrappedMessage = msg.WrappedMessage + $" x{LastMessage.repeat}";

foreach (var chat in _chats)
{
// chat._controller.History[index].Item2
chat.UpdateMessage(chat.GetHistoryLength() - 1, LastMessage);
}
return;
}

LastMessage = msg;

// Log all incoming chat to repopulate when filter is un-toggled
if (!msg.HideChat)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
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"
MinSize="465 225">
<PanelContainer Name="ChatWindowPanel" Access="Public" HorizontalExpand="True" VerticalExpand="True"
StyleClasses="StyleNano.StyleClassChatPanel">
<BoxContainer Orientation="Vertical" SeparationOverride="4" HorizontalExpand="True" VerticalExpand="True">
<OutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" />
<controls1:SunriseOutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" />
<controls:ChatInputBox HorizontalExpand="True" Name="ChatInput" Access="Public" Margin="2"/>
</BoxContainer>
</PanelContainer>
Expand Down
23 changes: 23 additions & 0 deletions Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,29 @@ private void OnChannelFilter(ChatChannel channel, bool active)
}
}

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);
Expand Down
289 changes: 289 additions & 0 deletions Content.Client/_Sunrise/UI/Controls/SunriseOutputPanel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
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;
}
}
}
Loading

0 comments on commit 79fd150

Please sign in to comment.