using System; using System.Collections.Generic; using System.Linq; using Content.Client.Alerts.UI; using Content.Client.Chat.Managers; using Content.Client.Lobby; using Content.Client.Resources; using Content.Client.Stylesheets; using Content.Shared.Chat; using Content.Shared.Input; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Timing; using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Chat.UI { public class ChatBox : Control { public const float InitialChatBottom = 235; public delegate void TextSubmitHandler(ChatBox chatBox, string text); public delegate void FilterToggledHandler(ChatChannel toggled, bool enabled); public event TextSubmitHandler? TextSubmitted; public event FilterToggledHandler? FilterToggled; public HistoryLineEdit Input { get; private set; } public OutputPanel Contents { get; } public event Action? OnResized; // order in which the available channel filters show up when available public static readonly IReadOnlyList ChannelFilterOrder = new List { ChatChannel.Local, ChatChannel.Emotes, ChatChannel.Radio, ChatChannel.OOC, ChatChannel.Dead, ChatChannel.AdminChat, ChatChannel.Server }; // order in which the channels show up in the channel selector private static readonly IReadOnlyList ChannelSelectorOrder = new List { ChatChannel.Local, ChatChannel.Emotes, ChatChannel.Radio, ChatChannel.OOC, ChatChannel.Dead, ChatChannel.AdminChat }; private const float FilterPopupWidth = 110; private const int DragMarginSize = 7; private const int MinDistanceFromBottom = 255; private const int MinLeft = 500; /// /// Will be Unspecified if set to Console /// public ChatChannel SelectedChannel = ChatChannel.Unspecified; /// /// Default formatting string for the ClientChatConsole. /// public string DefaultChatFormat { get; set; } = string.Empty; public bool ReleaseFocusOnEnter { get; set; } = true; public bool ClearOnEnter { get; set; } = true; // when channel is changed temporarily due to typing an alias // prefix, we save the current channel selection here to restore it when // the message is sent private ChatChannel? _savedSelectedChannel; private readonly Popup _channelSelectorPopup; private readonly Button _channelSelector; private readonly BoxContainer _channelSelectorHBox; private readonly FilterButton _filterButton; private readonly Popup _filterPopup; private readonly PanelContainer _filterPopupPanel; private readonly BoxContainer _filterVBox; private DragMode _currentDrag = DragMode.None; private Vector2 _dragOffsetTopLeft; private Vector2 _dragOffsetBottomRight; private readonly IClyde _clyde; private readonly bool _lobbyMode; private byte _clampIn; // currently known selectable channels as provided by ChatManager, // never contains Unspecified (which corresponds to Console which is always available) public List SelectableChannels = new(); /// /// When lobbyMode is false, will position / add to correct location in StateRoot and /// be resizable. /// wWen true, will leave layout up to parent and not be resizable. /// public ChatBox() { //TODO Paul needs to fix xaml ctor args so we can pass this instead of resolving it. var stateManager = IoCManager.Resolve(); _lobbyMode = stateManager.CurrentState is LobbyState; // TODO: Revisit the resizing stuff after https://github.com/space-wizards/RobustToolbox/issues/1392 is done, // Probably not "supposed" to inject IClyde, but I give up. // I can't find any other way to allow this control to properly resize when the // window is resized. Resized() isn't reliably called when resizing the window, // and layoutcontainer anchor / margin don't seem to adjust how we need // them to when the window is resized. We need it to be able to resize // within some bounds so that it doesn't overlap other UI elements, while still // being freely resizable within those bounds. _clyde = IoCManager.Resolve(); MouseFilter = MouseFilterMode.Stop; LayoutContainer.SetMarginLeft(this, 4); LayoutContainer.SetMarginRight(this, 4); MinHeight = 128; MinWidth = 200; AddChild(new PanelContainer { PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#25252aaa")}, VerticalExpand = true, HorizontalExpand = true, Children = { new BoxContainer { Orientation = LayoutOrientation.Vertical, Children = { (Contents = new OutputPanel { VerticalExpand = true, }), new PanelContainer { StyleClasses = { StyleNano.StyleClassChatSubPanel }, HorizontalExpand = true, Children = { new BoxContainer { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true, SeparationOverride = 4, Children = { (_channelSelector = new ChannelSelectorButton { StyleClasses = { StyleNano.StyleClassChatChannelSelectorButton }, MinWidth = 75, Text = Loc.GetString("hud-chatbox-ooc"), ToggleMode = true }), (Input = new HistoryLineEdit { PlaceHolder = Loc.GetString("hud-chatbox-info"), HorizontalExpand = true, StyleClasses = { StyleNano.StyleClassChatLineEdit } }), (_filterButton = new FilterButton { StyleClasses = { StyleNano.StyleClassChatFilterOptionButton } }) } } } } } } } }); _filterPopup = new Popup { Children = { (_filterPopupPanel = new PanelContainer { StyleClasses = {StyleNano.StyleClassBorderedWindowPanel}, Children = { new BoxContainer { Orientation = LayoutOrientation.Horizontal, Children = { new Control{MinSize = (4,0)}, (_filterVBox = new BoxContainer { Orientation = LayoutOrientation.Vertical, SeparationOverride = 4 }) } } } }) } }; _channelSelectorPopup = new Popup { Children = { (_channelSelectorHBox = new BoxContainer { Orientation = LayoutOrientation.Horizontal, SeparationOverride = 1 }) } }; if (!_lobbyMode) { UserInterfaceManager.StateRoot.AddChild(this); LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10); LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10); LayoutContainer.SetMarginLeft(this, -475); LayoutContainer.SetMarginBottom(this, InitialChatBottom); OnResized?.Invoke(new ChatResizedEventArgs(InitialChatBottom)); } } protected override void EnteredTree() { base.EnteredTree(); _channelSelector.OnToggled += OnChannelSelectorToggled; _filterButton.OnToggled += OnFilterButtonToggled; Input.OnKeyBindDown += InputKeyBindDown; Input.OnTextEntered += Input_OnTextEntered; Input.OnTextChanged += InputOnTextChanged; Input.OnFocusExit += InputOnFocusExit; _channelSelectorPopup.OnPopupHide += OnChannelSelectorPopupHide; _filterPopup.OnPopupHide += OnFilterPopupHide; _clyde.OnWindowResized += ClydeOnOnWindowResized; } protected override void ExitedTree() { base.ExitedTree(); _channelSelector.OnToggled -= OnChannelSelectorToggled; _filterButton.OnToggled -= OnFilterButtonToggled; Input.OnKeyBindDown -= InputKeyBindDown; Input.OnTextEntered -= Input_OnTextEntered; Input.OnTextChanged -= InputOnTextChanged; Input.OnFocusExit -= InputOnFocusExit; _channelSelectorPopup.OnPopupHide -= OnChannelSelectorPopupHide; _filterPopup.OnPopupHide -= OnFilterPopupHide; _clyde.OnWindowResized -= ClydeOnOnWindowResized; UnsubFilterItems(); UnsubChannelItems(); } private void UnsubFilterItems() { foreach (var child in _filterVBox.Children) { if (child is not ChannelFilterCheckbox checkbox) continue; checkbox.OnToggled -= OnFilterCheckboxToggled; } } private void UnsubChannelItems() { foreach (var child in _channelSelectorHBox.Children) { if (child is not ChannelItemButton button) continue; button.OnPressed -= OnChannelSelectorItemPressed; } } /// /// Update the available filters / selectable channels and the current filter settings using the provided /// data. /// /// channels currently selectable to send on /// channels currently able ot filter on /// current settings for the channel filters, this SHOULD always have an entry if /// there is a corresponding entry in filterableChannels, but it may also have additional /// entries (which should not be presented to the user) /// unread message counts for each disabled channel, values 10 or higher will show as 9+ public void SetChannelPermissions(List selectableChannels, IReadOnlySet filterableChannels, IReadOnlyDictionary channelFilters, IReadOnlyDictionary unreadMessages, bool switchIfConsole) { SelectableChannels = selectableChannels; // update the channel selector UnsubChannelItems(); _channelSelectorHBox.RemoveAllChildren(); foreach (var selectableChannel in ChannelSelectorOrder) { if (!selectableChannels.Contains(selectableChannel)) continue; var newButton = new ChannelItemButton(selectableChannel); newButton.OnPressed += OnChannelSelectorItemPressed; _channelSelectorHBox.AddChild(newButton); } // console channel is always selectable and represented via Unspecified var consoleButton = new ChannelItemButton(ChatChannel.Unspecified); consoleButton.OnPressed += OnChannelSelectorItemPressed; _channelSelectorHBox.AddChild(consoleButton); if (_savedSelectedChannel.HasValue && _savedSelectedChannel.Value != ChatChannel.Unspecified && !selectableChannels.Contains(_savedSelectedChannel.Value)) { // we just lost our saved selected channel, the current one will become permanent _savedSelectedChannel = null; } if (!selectableChannels.Contains(SelectedChannel) && (switchIfConsole || SelectedChannel != ChatChannel.Unspecified)) { // our previously selected channel no longer exists or we are still on console channel because we just joined if ((SelectedChannel & ChatChannel.IC) != 0 || SelectedChannel == ChatChannel.Unspecified) { if (!SafelySelectChannel(ChatChannel.Local)) SafelySelectChannel(ChatChannel.Dead); } else if (selectableChannels.Contains(ChatChannel.OOC)) { SafelySelectChannel(ChatChannel.OOC); } else //This shouldn't happen but better to be safe than sorry { SafelySelectChannel(selectableChannels.First()); } } else { SafelySelectChannel(SelectedChannel); } // update the channel filters UnsubFilterItems(); _filterVBox.Children.Clear(); _filterVBox.AddChild(new Control {CustomMinimumSize = (10, 0)}); foreach (var channelFilter in ChannelFilterOrder) { if (!filterableChannels.Contains(channelFilter)) continue; byte? unreadCount = null; if (unreadMessages.TryGetValue(channelFilter, out var unread)) { unreadCount = unread; } var newCheckBox = new ChannelFilterCheckbox(channelFilter, unreadCount) { // shouldn't happen, but if there's no explicit enable setting provided, default to enabled Pressed = !channelFilters.TryGetValue(channelFilter, out var enabled) || enabled }; newCheckBox.OnToggled += OnFilterCheckboxToggled; _filterVBox.AddChild(newCheckBox); } _filterVBox.AddChild(new Control {CustomMinimumSize = (10, 0)}); } /// /// Update the unread message counts in the filters based on the provided data. /// /// counts for each channel, any values above 9 will show as 9+ public void UpdateUnreadMessageCounts(IReadOnlyDictionary unreadMessages) { foreach (var channelFilter in _filterVBox.Children) { if (channelFilter is not ChannelFilterCheckbox filterCheckbox) continue; if (unreadMessages.TryGetValue(filterCheckbox.Channel, out var unread)) { filterCheckbox.UpdateUnreadCount(unread); } else { filterCheckbox.UpdateUnreadCount(null); } } } private void OnFilterCheckboxToggled(BaseButton.ButtonToggledEventArgs args) { if (args.Button is not ChannelFilterCheckbox checkbox) return; FilterToggled?.Invoke(checkbox.Channel, checkbox.Pressed); } private void OnFilterButtonToggled(BaseButton.ButtonToggledEventArgs args) { if (args.Pressed) { var globalPos = _filterButton.GlobalPosition; var (minX, minY) = _filterPopupPanel.CombinedMinimumSize; var box = UIBox2.FromDimensions(globalPos - (FilterPopupWidth, 0), (Math.Max(minX, FilterPopupWidth), minY)); UserInterfaceManager.ModalRoot.AddChild(_filterPopup); _filterPopup.Open(box); } else { _filterPopup.Close(); } } private void OnChannelSelectorToggled(BaseButton.ButtonToggledEventArgs args) { if (args.Pressed) { var globalLeft = GlobalPosition.X; var globalBot = GlobalPosition.Y + Height; var box = UIBox2.FromDimensions((globalLeft, globalBot), (SizeBox.Width, AlertsUI.ChatSeparation)); UserInterfaceManager.ModalRoot.AddChild(_channelSelectorPopup); _channelSelectorPopup.Open(box); } else { _channelSelectorPopup.Close(); } } private void OnFilterPopupHide() { OnPopupHide(_filterPopup, _filterButton); } private void OnChannelSelectorPopupHide() { OnPopupHide(_channelSelectorPopup, _channelSelector); } private void OnPopupHide(Control popup, BaseButton button) { UserInterfaceManager.ModalRoot.RemoveChild(popup); // this weird check here is because the hiding of the popup happens prior to the button // receiving the keydown, which would cause it to then become unpressed // and reopen immediately. To avoid this, if the popup was hidden due to clicking on the button, // we will not auto-unpress the button, instead leaving it up to the button toggle logic // (and this requires the button to be set to EnableAllKeybinds = true) if (UserInterfaceManager.CurrentlyHovered != button) { button.Pressed = false; } } private void OnChannelSelectorItemPressed(BaseButton.ButtonEventArgs obj) { if (obj.Button is not ChannelItemButton button) return; SafelySelectChannel(button.Channel); _channelSelectorPopup.Close(); } /// /// Selects the indicated channel, clearing out any temporarily-selected channel /// (any currently entered text is preserved). If the specified channel is not selectable, /// will just maintain current selection. /// public void SelectChannel(ChatChannel toSelect) { _savedSelectedChannel = null; SafelySelectChannel(toSelect); } private bool SafelySelectChannel(ChatChannel toSelect) { if (toSelect == ChatChannel.Unspecified || SelectableChannels.Contains(toSelect)) { SelectedChannel = toSelect; _channelSelector.Text = ChannelSelectorName(toSelect); _channelSelector.Modulate = ChatHelper.ChatColor(toSelect); return true; } // keep current setting return false; } protected override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (args.Function == EngineKeyFunctions.UIClick && !_lobbyMode) { _currentDrag = GetDragModeFor(args.RelativePosition); if (_currentDrag != DragMode.None) { _dragOffsetTopLeft = args.PointerLocation.Position / UIScale - Position; _dragOffsetBottomRight = Position + Size - args.PointerLocation.Position / UIScale; } } if (args.CanFocus) { Input.GrabKeyboardFocus(); } } protected override void KeyBindUp(GUIBoundKeyEventArgs args) { base.KeyBindUp(args); if (args.Function != EngineKeyFunctions.UIClick || _lobbyMode) { return; } _dragOffsetTopLeft = _dragOffsetBottomRight = Vector2.Zero; _currentDrag = DragMode.None; // If this is done in MouseDown, Godot won't fire MouseUp as you need focus to receive MouseUps. UserInterfaceManager.KeyboardFocused?.ReleaseKeyboardFocus(); } public void CycleChatChannel(bool forward) { Input.IgnoreNext = true; var channels = SelectableChannels; var idx = channels.IndexOf(SelectedChannel); if (forward) { idx++; } else { idx--; } idx = MathHelper.Mod(idx, channels.Count); SelectChannel(channels[idx]); } private void InputKeyBindDown(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.TextReleaseFocus) { Input.ReleaseKeyboardFocus(); args.Handle(); return; } if (args.Function == ContentKeyFunctions.CycleChatChannelForward) { CycleChatChannel(true); args.Handle(); return; } if (args.Function == ContentKeyFunctions.CycleChatChannelBackward) { CycleChatChannel(false); args.Handle(); return; } // if we temporarily selected another channel via a prefx, undo that when we backspace on an empty input if (args.Function == EngineKeyFunctions.TextBackspace && Input.Text.Length == 0 && _savedSelectedChannel.HasValue) { SafelySelectChannel(_savedSelectedChannel.Value); _savedSelectedChannel = null; } } // TODO: this drag and drop stuff is somewhat duplicated from Robust BaseWindow but also modified [Flags] private enum DragMode : byte { None = 0, Bottom = 1 << 1, Left = 1 << 2 } private DragMode GetDragModeFor(Vector2 relativeMousePos) { var mode = DragMode.None; if (relativeMousePos.Y > Size.Y - DragMarginSize) { mode = DragMode.Bottom; } if (relativeMousePos.X < DragMarginSize) { mode |= DragMode.Left; } return mode; } protected override void MouseMove(GUIMouseMoveEventArgs args) { base.MouseMove(args); if (Parent == null || _lobbyMode) { return; } if (_currentDrag == DragMode.None) { var cursor = CursorShape.Arrow; var previewDragMode = GetDragModeFor(args.RelativePosition); switch (previewDragMode) { case DragMode.Bottom: cursor = CursorShape.VResize; break; case DragMode.Left: cursor = CursorShape.HResize; break; case DragMode.Bottom | DragMode.Left: cursor = CursorShape.Crosshair; break; } DefaultCursorShape = cursor; } else { var top = Rect.Top; var bottom = Rect.Bottom; var left = Rect.Left; var right = Rect.Right; var (minSizeX, minSizeY) = CombinedMinimumSize; if ((_currentDrag & DragMode.Bottom) == DragMode.Bottom) { bottom = Math.Max(args.GlobalPosition.Y + _dragOffsetBottomRight.Y, top + minSizeY); } if ((_currentDrag & DragMode.Left) == DragMode.Left) { var maxX = right - minSizeX; left = Math.Min(args.GlobalPosition.X - _dragOffsetTopLeft.X, maxX); } ClampSize(left, bottom); } } protected override void UIScaleChanged() { base.UIScaleChanged(); ClampAfterDelay(); } private void ClydeOnOnWindowResized(WindowResizedEventArgs obj) { ClampAfterDelay(); } private void ClampAfterDelay() { if (!_lobbyMode) _clampIn = 2; } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); if (_lobbyMode) return; // we do the clamping after a delay (after UI scale / window resize) // because we need to wait for our parent container to properly resize // first, so we can calculate where we should go. If we do it right away, // we won't have the correct values from the parent to know how to adjust our margins. if (_clampIn <= 0) return; _clampIn -= 1; if (_clampIn == 0) ClampSize(); } private void ClampSize(float? desiredLeft = null, float? desiredBottom = null) { if (Parent == null || _lobbyMode) return; var top = Rect.Top; var right = Rect.Right; var left = desiredLeft ?? Rect.Left; var bottom = desiredBottom ?? Rect.Bottom; // clamp so it doesn't go too high or low (leave space for alerts UI) var maxBottom = Parent.Size.Y - MinDistanceFromBottom; if (maxBottom <= MinHeight) { // we can't fit in our given space (window made awkwardly small), so give up // and overlap at our min height bottom = MinHeight; } else { bottom = Math.Clamp(bottom, MinHeight, maxBottom); } var maxLeft = Parent.Size.X - MinWidth; if (maxLeft <= MinLeft) { // window too narrow, give up and overlap at our max left left = maxLeft; } else { left = Math.Clamp(left, MinLeft, maxLeft); } LayoutContainer.SetMarginLeft(this, -((right + 10) - left)); LayoutContainer.SetMarginBottom(this, bottom); OnResized?.Invoke(new ChatResizedEventArgs(bottom)); } protected override void MouseExited() { base.MouseExited(); if (_currentDrag == DragMode.None && !_lobbyMode) { DefaultCursorShape = CursorShape.Arrow; } } private void InputOnTextChanged(LineEdit.LineEditEventArgs obj) { // switch temporarily to a different channel if an alias prefix has been entered. // are we already temporarily switching to a channel? if (_savedSelectedChannel.HasValue) return; var trimmed = obj.Text.Trim(); if (trimmed.Length == 0 || trimmed.Length > 1) return; var channel = GetChannelFromPrefix(trimmed[0]); var prevChannel = SelectedChannel; if (channel == null || !SafelySelectChannel(channel.Value)) return; // we ate the prefix and auto-switched (temporarily) to the channel with that prefix _savedSelectedChannel = prevChannel; Input.Text = ""; } private static ChatChannel? GetChannelFromPrefix(char prefix) { return prefix switch { ChatManager.MeAlias => ChatChannel.Emotes, ChatManager.RadioAlias => ChatChannel.Radio, ChatManager.AdminChatAlias => ChatChannel.AdminChat, ChatManager.OOCAlias => ChatChannel.OOC, ChatManager.ConCmdSlash => ChatChannel.Unspecified, _ => null }; } private static string GetPrefixFromChannel(ChatChannel channel) { char? prefixChar = channel switch { ChatChannel.Emotes => ChatManager.MeAlias, ChatChannel.Radio => ChatManager.RadioAlias, ChatChannel.AdminChat => ChatManager.AdminChatAlias, ChatChannel.OOC => ChatManager.OOCAlias, ChatChannel.Unspecified => ChatManager.ConCmdSlash, _ => null }; return prefixChar.ToString() ?? string.Empty; } public static string ChannelSelectorName(ChatChannel channel) { return channel switch { ChatChannel.AdminChat => Loc.GetString("hud-chatbox-admin"), ChatChannel.Unspecified => Loc.GetString("hud-chatbox-console"), _ => Loc.GetString(channel.ToString()) }; } public void AddLine(string message, ChatChannel channel, Color color) { if (Disposed) { return; } var formatted = new FormattedMessage(3); formatted.PushColor(color); formatted.AddMarkup(message); formatted.Pop(); Contents.AddMessage(formatted); } private void InputOnFocusExit(LineEdit.LineEditEventArgs obj) { // undo the temporary selection, otherwise it will be odd if user // comes back to it later only to have their selection cleared upon sending if (!_savedSelectedChannel.HasValue) return; SafelySelectChannel(_savedSelectedChannel.Value); _savedSelectedChannel = null; } private void Input_OnTextEntered(LineEdit.LineEditEventArgs args) { // We set it there to true so it's set to false by TextSubmitted.Invoke if necessary ClearOnEnter = true; if (!string.IsNullOrWhiteSpace(args.Text)) { TextSubmitted?.Invoke(this, GetPrefixFromChannel(SelectedChannel) + args.Text); } if (ClearOnEnter) { Input.Clear(); if (_savedSelectedChannel.HasValue) { SafelySelectChannel(_savedSelectedChannel.Value); _savedSelectedChannel = null; } } if (ReleaseFocusOnEnter) { Input.ReleaseKeyboardFocus(); } } } /// /// Only needed to avoid the issue where right click on the button closes the popup /// but leaves the button highlighted. /// public sealed class ChannelSelectorButton : Button { public ChannelSelectorButton() { // needed so the popup is untoggled regardless of which key is pressed when hovering this button. // If we don't have this, then right clicking the button while it's toggled on will hide // the popup but keep the button toggled on Mode = ActionMode.Press; EnableAllKeybinds = true; } protected override void KeyBindDown(GUIBoundKeyEventArgs args) { // needed since we need EnableAllKeybinds - don't double-send both UI click and Use if (args.Function == EngineKeyFunctions.Use) return; base.KeyBindDown(args); } } public sealed class FilterButton : ContainerButton { private static readonly Color ColorNormal = Color.FromHex("#7b7e9e"); private static readonly Color ColorHovered = Color.FromHex("#9699bb"); private static readonly Color ColorPressed = Color.FromHex("#789B8C"); private readonly TextureRect _textureRect; public FilterButton() { var filterTexture = IoCManager.Resolve() .GetTexture("/Textures/Interface/Nano/filter.svg.96dpi.png"); // needed for same reason as ChannelSelectorButton Mode = ActionMode.Press; EnableAllKeybinds = true; AddChild( (_textureRect = new TextureRect { Texture = filterTexture, SizeFlagsVertical = SizeFlags.ShrinkCenter, SizeFlagsHorizontal = SizeFlags.ShrinkCenter }) ); ToggleMode = true; } protected override void KeyBindDown(GUIBoundKeyEventArgs args) { // needed since we need EnableAllKeybinds - don't double-send both UI click and Use if (args.Function == EngineKeyFunctions.Use) return; base.KeyBindDown(args); } private void UpdateChildColors() { if (_textureRect == null) return; switch (DrawMode) { case DrawModeEnum.Normal: _textureRect.ModulateSelfOverride = ColorNormal; break; case DrawModeEnum.Pressed: _textureRect.ModulateSelfOverride = ColorPressed; break; case DrawModeEnum.Hover: _textureRect.ModulateSelfOverride = ColorHovered; break; case DrawModeEnum.Disabled: break; } } protected override void DrawModeChanged() { base.DrawModeChanged(); UpdateChildColors(); } protected override void StylePropertiesChanged() { base.StylePropertiesChanged(); UpdateChildColors(); } } public sealed class ChannelItemButton : Button { public readonly ChatChannel Channel; public ChannelItemButton(ChatChannel channel) { Channel = channel; AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton); Text = ChatBox.ChannelSelectorName(channel); } } public sealed class ChannelFilterCheckbox : CheckBox { public readonly ChatChannel Channel; public ChannelFilterCheckbox(ChatChannel channel, byte? unreadCount) { Channel = channel; UpdateText(unreadCount); } private void UpdateText(byte? unread) { var name = Channel switch { ChatChannel.AdminChat => Loc.GetString("hud-chatbox-admin"), ChatChannel.Unspecified => throw new InvalidOperationException( "cannot create chat filter for Unspecified"), _ => Loc.GetString(Channel.ToString()) }; if (unread > 0) { Text = name + " (" + (unread > 9 ? "9+" : unread) + ")"; } else { Text = name; } } public void UpdateUnreadCount(byte? unread) { UpdateText(unread); } } public readonly struct ChatResizedEventArgs { /// new bottom that the chat rect is going to have in virtual pixels /// after the imminent relayout public readonly float NewBottom; public ChatResizedEventArgs(float newBottom) { NewBottom = newBottom; } } }