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); } }