namespace TinyChat;
///
/// A flow layout panel control that manages and displays chat message history with automatic scrolling and width management.
///
public class FlowLayoutMessageHistoryControl : FlowLayoutPanel, IChatMessageHistoryControl
{
private bool _shouldFollowStreamScroll = true;
///
/// Gets the maximum vertical scroll value that indicates the bottom of the scrollable area.
///
private int MaxVerticalScroll => VerticalScroll.Maximum - VerticalScroll.LargeChange;
///
/// Initializes a new instance of the class
/// with top-down flow direction, auto-scroll enabled, and content wrapping disabled.
///
public FlowLayoutMessageHistoryControl()
{
FlowDirection = FlowDirection.TopDown;
AutoScroll = true;
WrapContents = false;
}
///
/// Appends a chat message control to the history and automatically scrolls to show the new message.
///
/// The chat message control to append to the history.
public void AppendMessageControl(IChatMessageControl messageControl)
{
var control = (Control)messageControl;
Controls.Add(control);
SetMaxWidthToPreventHorizontalScrollbar(control);
ScrollControlIntoView(control);
messageControl.SizeUpdatedWhileStreaming += MessageControlStreamingSizeUpdate;
}
///
/// Clears all message controls from the chat history.
///
public void ClearMessageControls()
{
foreach (var messageControl in Controls.OfType())
messageControl.SizeUpdatedWhileStreaming -= MessageControlStreamingSizeUpdate;
Controls.Clear();
}
///
/// Removes the message control associated with the specified chat message from the history.
///
/// The chat message whose control should be removed.
public void RemoveMessageControl(IChatMessage message)
{
if (Controls.OfType().FirstOrDefault(mc => mc.Message?.Equals(message) ?? false) is { } messageControl)
{
messageControl.SizeUpdatedWhileStreaming -= MessageControlStreamingSizeUpdate;
Controls.Remove((Control)messageControl);
}
}
///
/// Handles the client size changed event by updating the maximum width of all child controls
/// to prevent horizontal scrollbars from appearing.
///
/// The event arguments containing information about the size change.
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
SuspendLayout();
foreach (Control control in Controls)
SetMaxWidthToPreventHorizontalScrollbar(control);
ResumeLayout();
PerformLayout(); // to hide the H-scrollbar that pops up from time to time
}
///
/// Sets the maximum width of a control to prevent horizontal scrollbars by accounting for
/// the vertical scrollbar width when present.
///
/// The control whose maximum width should be adjusted.
private void SetMaxWidthToPreventHorizontalScrollbar(Control control)
{
control.MaximumSize = new Size(ClientRectangle.Width - SystemInformation.VerticalScrollBarWidth, 0);
}
///
protected override void OnScroll(ScrollEventArgs se)
{
base.OnScroll(se);
var didScrollUp = se.ScrollOrientation == ScrollOrientation.VerticalScroll && se.NewValue < se.OldValue;
var didScrollToBottom = se.NewValue >= MaxVerticalScroll;
_shouldFollowStreamScroll = !didScrollUp && didScrollToBottom;
}
///
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
var didScrollUp = e.Delta > 0;
var didScrollToBottom = VerticalScroll.Value >= MaxVerticalScroll;
_shouldFollowStreamScroll = !didScrollUp && didScrollToBottom;
}
private void MessageControlStreamingSizeUpdate(object? sender, EventArgs args)
{
// can't use ScrollControlIntoView() because this will stop scrolling
// once the message controls gets larger than the flow layout panel
if (_shouldFollowStreamScroll)
BeginInvoke(() => VerticalScroll.Value = MaxVerticalScroll);
}
}