From faf5b2a7f8050cf4ffd537c4105fe70d4402a545 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 00:11:01 -0400 Subject: [PATCH 1/8] feat: deep fryer ui --- .../BUI/DeepFryerBoundUserInterface.cs | 49 ++++ .../_DV/Kitchen/UI/Controls/FryerBaskets.xaml | 18 ++ .../Kitchen/UI/Controls/FryerBaskets.xaml.cs | 210 +++++++++++++++ .../Kitchen/UI/Controls/FryerItemButton.xaml | 18 ++ .../UI/Controls/FryerItemButton.xaml.cs | 95 +++++++ .../_DV/Kitchen/UI/DeepFryerWindow.xaml | 76 ++++++ .../_DV/Kitchen/UI/DeepFryerWindow.xaml.cs | 246 ++++++++++++++++++ .../Sheetlets/AdvancedProgressBarSheetlet.cs | 37 +++ .../Sheetlets/DeepFryerSheetlet.cs | 42 +++ .../Controls/AdvancedProgressBar.xaml | 8 + .../Controls/AdvancedProgressBar.xaml.cs | 144 ++++++++++ .../UserInterfaces/Controls/StatusLight.xaml | 20 ++ .../Controls/StatusLight.xaml.cs | 244 +++++++++++++++++ Content.Server/_DV/Kitchen/DeepFryerSystem.cs | 44 ++++ .../BUI/DeepFryerBoundUserInterfaceState.cs | 23 ++ .../Kitchen/Systems/SharedDeepFryerSystem.cs | 31 ++- .../_DV/kitchen/deep-fryer-component.ftl | 10 +- .../Structures/Machines/deep_fryer.yml | 8 +- 18 files changed, 1319 insertions(+), 4 deletions(-) create mode 100644 Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs create mode 100644 Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml create mode 100644 Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs create mode 100644 Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml create mode 100644 Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs create mode 100644 Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml create mode 100644 Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs create mode 100644 Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs create mode 100644 Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs create mode 100644 Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml create mode 100644 Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs create mode 100644 Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml create mode 100644 Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs create mode 100644 Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs diff --git a/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs b/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs new file mode 100644 index 00000000000..0fa18dbc3ac --- /dev/null +++ b/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Content.Client._DV.Kitchen.UI; +using Content.Shared._DV.Kitchen.BUI; +using Content.Shared._DV.Kitchen.Systems; +using Robust.Client.Player; +using Robust.Client.UserInterface; +namespace Content.Client._DV.Kitchen.BUI; + +public sealed class DeepFryerBoundUserInterface : BoundUserInterface +{ + private DeepFryerWindow? _window; + + [Dependency] + private IPlayerManager _player = default!; + + public DeepFryerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + IoCManager.InjectDependencies(this); + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + + _window.OnFoodItemPressed += (item) => + { + SendPredictedMessage(new DeepFryerTryEjectItemMessage(EntMan.GetNetEntity(item), EntMan.GetNetEntity(_player.LocalEntity))); + }; + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (_window is null || state is not DeepFryerBoundUserInterfaceState deepFryerState) + return; + + _window.OilQuality = deepFryerState.OilQuality; + _window.SolutionColor = deepFryerState.SolutionColor; + _window.MinimumVolume = deepFryerState.MinimumVolume; + _window.SolutionVolume = deepFryerState.SolutionVolume; + _window.SolutionMaxVolume = deepFryerState.SolutionMaxVolume; + _window.CookingItems = [..deepFryerState.CookingItems.Select(EntMan.GetEntity)]; + _window.IsPowered = deepFryerState.IsPowered; + _window.Capacity = deepFryerState.Capacity; + } +} diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml new file mode 100644 index 00000000000..af046f6b60f --- /dev/null +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs new file mode 100644 index 00000000000..7587c86cd37 --- /dev/null +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs @@ -0,0 +1,210 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Utility; + +namespace Content.Client._DV.Kitchen.UI.Controls; + +[GenerateTypedNameReferences] +public sealed partial class FryerBaskets : PanelContainer +{ + public const string ContentContainerStyleClass = "FryerBasketsContainer"; + + [Dependency] + private readonly IEntityManager _entityManager = default!; + + private readonly SpriteSystem _sprite; + + + private readonly List _itemControls = []; + private readonly EntityQuery _metadataQuery; + + /// + /// Event raised when pressing a that does have an assigned . + /// + [PublicAPI] + public event Action? OnFoodItemPressed; + + /// + /// Event raised when pressing a that does not have an assigned . + /// + [PublicAPI] + public event Action? OnEmptyItemPressed; + + /// + /// The maximum number of items that can be inside the deep fryer. + /// + [PublicAPI] + public int Capacity + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } = 3; + + /// + /// The read-only list of s inside the deep fryer. + /// + [PublicAPI] + public IReadOnlyList Contents + { + get; + set + { + if (Equals(field, value)) + return; + + field = value; + BindValues(); + } + } = []; + + /// + /// When true, buttons for empty fryer item slots will be shown. + /// + [PublicAPI] + public bool ShowEmptyBaskets + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } + + /// + /// The optional of the text to display in empty fryer item buttons. + /// + [PublicAPI] + public LocId? EmptyBasketText + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } + + /// + /// The of the container which holds the fryer item buttons. + /// + [PublicAPI] + public Thickness InnerMargin + { + get => ContentsContainer.Margin; + set => ContentsContainer.Margin = value; + } + + public FryerBaskets() + { + RobustXamlLoader.Load(this); + + IoCManager.InjectDependencies(this); + + _sprite = _entityManager.System(); + _metadataQuery = _entityManager.GetEntityQuery(); + + TooltipSupplier = CreateTooltip; + } + + private Tooltip? CreateTooltip(Control sender) + { + if (ToolTip is null) + return null; + + var tooltip = new Tooltip(); + tooltip.SetMessage(FormattedMessage.FromMarkupPermissive(ToolTip)); + + return tooltip; + } + + private void BindValues() + { + var expectedButtons = Math.Max(Capacity, Contents.Count); + + while (_itemControls.Count < expectedButtons) + { + var newBtn = new FryerItemButton(); + newBtn.OnItemPressed += OnItemButtonPressed; + _itemControls.Add(newBtn); + ContentsContainer.AddChild(newBtn); + } + + for (var i = 0; i < _itemControls.Count; i++) + { + var fryerItemButton = _itemControls[i]; + + if (i < Contents.Count) + { + fryerItemButton.ItemEntity = Contents[i]; + fryerItemButton.ItemTexture = TryGetTextureForEntity(Contents[i], out var texture) ? texture : null; + + fryerItemButton.Text = _metadataQuery.TryGetComponent(Contents[i], out var metadata) ? metadata.EntityName : null; + fryerItemButton.Visible = true; + } + else + { + fryerItemButton.ItemEntity = null; + fryerItemButton.ItemTexture = null; + + fryerItemButton.Text = Loc.GetString(EmptyBasketText ?? "deep-fryer-ui-empty"); + fryerItemButton.Visible = ShowEmptyBaskets; + } + } + } + + private void OnItemButtonPressed(FryerItemButton.ButtonPressedEventArgs args) + { + if(args.IsEmpty) + { + OnEmptyItemPressed?.Invoke(); + } + else + { + OnFoodItemPressed?.Invoke(args.ItemEntity.Value); + } + } + + private bool TryGetTextureForEntity(EntityUid ent, [NotNullWhen(true)] out Texture? texture) + { + texture = null; + + if (_entityManager.Deleted(ent)) + return false; + + if (_entityManager.TryGetComponent(ent, out var icon)) + { + texture = _sprite.GetIcon(icon); + return true; + } + + if (_entityManager.TryGetComponent(ent, out var sprite) && sprite.Icon is not null) + { + texture = sprite.Icon.Default; + return true; + } + + return false; + } +} + diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml new file mode 100644 index 00000000000..3a8ad9eff79 --- /dev/null +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs new file mode 100644 index 00000000000..075dfd78591 --- /dev/null +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs @@ -0,0 +1,95 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._DV.Kitchen.UI.Controls; + +[GenerateTypedNameReferences] +public sealed partial class FryerItemButton : ContainerButton +{ + /// + /// Event raised when pressing this button. See . + /// + [PublicAPI] + public event Action? OnItemPressed; + + /// + /// The of the item associated with this button, if any. + /// + [PublicAPI] + public EntityUid? ItemEntity + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } + + /// + /// The texture of the item icon. This can be left null for a text-only button. + /// + [PublicAPI] + public Texture? ItemTexture + { + get => ItemIcon.Texture; + set => ItemIcon.Texture = value; + } + + /// + /// The text to display on the button (e.g., the item's name). This is optional and can be left null for an icon-only button. + /// + [PublicAPI] + public string? Text + { + get => TextLabel.Text; + set => TextLabel.Text = value; + } + + public FryerItemButton() + { + RobustXamlLoader.Load(this); + + OnPressed += OnButtonPressed; + } + + private void OnButtonPressed(ButtonEventArgs args) + { + OnItemPressed?.Invoke(new ButtonPressedEventArgs(this, args.Event, ItemEntity)); + } + + private void BindValues() + { + ToolTip = ItemEntity.HasValue ? Loc.GetString("deep-fryer-eject-item", ("item", ItemEntity)) : null; + } + + /// + /// Event arguments for the event. + /// + /// + /// + /// + public sealed class ButtonPressedEventArgs(BaseButton button, GUIBoundKeyEventArgs args, EntityUid? itemEntity) : ButtonEventArgs(button, args) + { + /// + /// The of the item associated with this button, if any. + /// + [PublicAPI] + public EntityUid? ItemEntity { get; } = itemEntity; + + /// + /// Whether the button is empty (i.e., has no item entity). + /// + [PublicAPI] + [MemberNotNullWhen(false, nameof(ItemEntity))] + public bool IsEmpty => ItemEntity is null; + } +} diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml new file mode 100644 index 00000000000..adf509ffe45 --- /dev/null +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs new file mode 100644 index 00000000000..403eb7cd9d1 --- /dev/null +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs @@ -0,0 +1,246 @@ +using System.Numerics; +using Content.Client._DV.Kitchen.UI.Controls; +using Content.Shared._DV.Kitchen.Components; +using Content.Shared._DV.Kitchen.Systems; +using Content.Shared.FixedPoint; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Utility; + +namespace Content.Client._DV.Kitchen.UI; + +[GenerateTypedNameReferences] +public sealed partial class DeepFryerWindow : BaseWindow +{ + private const int DragMarginSize = 7; + + [Dependency] + private readonly IEntityManager _entityManager = default!; + + private readonly SharedDeepFryerSystem _deepFryer; + + private FormattedMessage? _oilQualityTooltip; + + /// + /// Event raised when pressing a with an assigned . + /// + [PublicAPI] + public event Action? OnFoodItemPressed; + + /// + /// Event raised when pressing a without an assigned . + /// + [PublicAPI] + public event Action? OnEmptyItemPressed; + + /// + /// The quality value of the oil. + /// + [PublicAPI] + public float OilQuality + { + get; + set + { + if (MathHelper.CloseTo(field, value)) + return; + + field = value; + BindValues(); + } + } = 1.0f; + + /// + /// The minimum volume of the deep fryer's oil solution to allow cooking. + /// + [PublicAPI] + public FixedPoint2 MinimumVolume + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } + + /// + /// The current color of the deep fryer's oil solution. + /// + [PublicAPI] + public Color SolutionColor + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } = Color.Yellow; + + /// + /// The current volume of the deep fryer's oil solution. + /// + [PublicAPI] + public FixedPoint2 SolutionVolume + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } + + /// + /// The maximum volume of the deep fryer's oil solution. + /// + [PublicAPI] + public FixedPoint2 SolutionMaxVolume + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + + } + + /// + /// The maximum number of items that can be inside the deep fryer. + /// + [PublicAPI] + public int Capacity + { + get; + set + { + if (field == value) + return; + + field = value; + BindValues(); + } + } = 3; + + /// + /// Whether the deep fryer is powered or not. + /// + [PublicAPI] + public bool IsPowered + { + get; + set + { + field = value; + BindValues(); + } + } + + /// + /// The entities contained in the deep fryer. + /// + [PublicAPI] + public IReadOnlyList CookingItems { get; set; } = []; + + /// + /// The level of the oil, as determined by . + /// + private OilQuality OilQualityCategory => SharedDeepFryerSystem.GetOilQualityLevel(OilQuality); + + public DeepFryerWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _deepFryer = _entityManager.System(); + + BasketControl.TooltipSupplier = BuildOilMeterTooltip; + + CloseButton.OnPressed += _ => Close(); + FryerBaskets.OnEmptyItemPressed += () => OnEmptyItemPressed?.Invoke(); + FryerBaskets.OnFoodItemPressed += entity => OnFoodItemPressed?.Invoke(entity); + + BindValues(); + } + + protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + { + var mode = DragMode.Move; + + if (Resizable) + { + if (relativeMousePos.Y < DragMarginSize) + { + mode = DragMode.Top; + } + else if (relativeMousePos.Y > Size.Y - DragMarginSize) + { + mode = DragMode.Bottom; + } + + if (relativeMousePos.X < DragMarginSize) + { + mode |= DragMode.Left; + } + else if (relativeMousePos.X > Size.X - DragMarginSize) + { + mode |= DragMode.Right; + } + } + + return mode; + } + + private (Color color, string labelName) GetOilQualityInfo() => _deepFryer.GetOilQualityInfo(OilQualityCategory); + + private Tooltip? BuildOilMeterTooltip(Control control) + { + if (SolutionVolume.Value == 0 || _oilQualityTooltip is null) return null; + + var tooltip = new Tooltip + { + Tracking = true + }; + + tooltip.SetMessage(_oilQualityTooltip); + + return tooltip; + } + + private void BindValues() + { + PowerIndicator.IsOn = IsPowered; + + OilMeter.MaximumValue = SolutionMaxVolume.Value; + OilMeter.Value = SolutionVolume.Value; + OilMeter.ForegroundColor = SolutionColor.WithAlpha(0.8f); + + OilIndicator.IsOn = IsPowered && SolutionVolume < MinimumVolume; + OilIndicator.ToolTip = OilIndicator.IsOn ? Loc.GetString("deep-fryer-ui-oil-tooltip", ("units", MinimumVolume)) : null; + + var (color, labelName) = GetOilQualityInfo(); + _oilQualityTooltip = FormattedMessage.FromMarkupPermissive(Loc.GetString("deep-fryer-oil-quality-examine", + ("color", color.ToHex()), + ("state", labelName))); + + FryerBaskets.Contents = CookingItems; + FryerBaskets.Capacity = Capacity; + } +} diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs new file mode 100644 index 00000000000..2632125a296 --- /dev/null +++ b/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs @@ -0,0 +1,37 @@ +using Content.Client._DV.UserInterfaces.Controls; +using Content.Client.Stylesheets; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; + +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client._DV.Stylesheets.Sheetlets; + +[CommonSheetlet] +public sealed class AdvancedProgressBarSheetlet : Sheetlet where T : PalettedStylesheet +{ + + public override StyleRule[] GetRules(T sheet, object config) + { + var bgBox = new StyleBoxFlat(sheet.PrimaryPalette.BackgroundDark); + var fgBox = new StyleBoxFlat(sheet.PrimaryPalette.BackgroundLight); + + var colorable = new StyleBoxFlat(Color.White); + + return + [ + E() + .ParentOf(E().Class(AdvancedProgressBar.BackgroundStyleClass)) + .Panel(bgBox), + + E() + .ParentOf(E().Class(AdvancedProgressBar.ForegroundStyleClass)) + .Panel(fgBox), + + E() + .ParentOf(E().Class(AdvancedProgressBar.ColorableStyleClass)) + .Panel(colorable), + ]; + } +} diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs new file mode 100644 index 00000000000..b3b27faf4e9 --- /dev/null +++ b/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs @@ -0,0 +1,42 @@ +using Content.Client._DV.Kitchen.UI.Controls; +using Content.Client.Stylesheets; +using Content.Client.Stylesheets.SheetletConfigs; +using Content.Client.Stylesheets.Sheetlets; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client._DV.Stylesheets.Sheetlets; + +[CommonSheetlet] +public sealed class DeepFryerSheetlet : Sheetlet where T : PalettedStylesheet, IButtonConfig, IIconConfig +{ + public override StyleRule[] GetRules(T sheet, object config) + { + var itemWrapperBox = new StyleBoxFlat(sheet.SecondaryPalette.BackgroundDark.WithAlpha(0.5f)); + var buttonBox = new StyleBoxFlat(sheet.SecondaryPalette.BackgroundLight.WithAlpha(0.90f)); + + var rules = new List + { + E() + .Panel(itemWrapperBox), + + E() + .Class(FryerBaskets.ContentContainerStyleClass) + .Prop(BoxContainer.StylePropertySeparation, 5), + + E() + .Box(buttonBox), + + E() + .ParentOf(E()) + .Prop(BoxContainer.StylePropertySeparation, 5), + }; + + ButtonSheetlet + .MakeButtonRules(rules, sheet.SecondaryPalette, null); + + return rules.ToArray(); + } +} diff --git a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml new file mode 100644 index 00000000000..59e59479ef4 --- /dev/null +++ b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs new file mode 100644 index 00000000000..9dd705665e3 --- /dev/null +++ b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs @@ -0,0 +1,144 @@ +using System.Numerics; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +namespace Content.Client._DV.UserInterfaces.Controls; + +[GenerateTypedNameReferences] +public sealed partial class AdvancedProgressBar : Control +{ + public const string BackgroundStyleClass = "Background"; + public const string ForegroundStyleClass = "Foreground"; + public const string ColorableStyleClass = "Colorable"; + + public enum BarOrientation : byte + { + RightToLeft, + LeftToRight, + TopToBottom, + BottomToTop + } + + [PublicAPI] + public BarOrientation Orientation + { + get; + set + { + field = value; + BindValues(); + } + } = BarOrientation.LeftToRight; + + [PublicAPI] + public int MinimumValue + { + get; + set + { + field = value; + BindValues(); + } + } = 0; + + [PublicAPI] + public int MaximumValue + { + get; + set + { + field = value; + BindValues(); + } + } = 100; + + [PublicAPI] + public int Value + { + get; + set + { + field = Math.Clamp(value, MinimumValue, MaximumValue); + BindValues(); + } + } = 0; + + [PublicAPI] + public float Percent => MaximumValue == 0 ? 0 : (float) Value / MaximumValue; + + [PublicAPI] + public Color? ForegroundColor + { + get => Foreground.ModulateSelfOverride; + set => Foreground.ModulateSelfOverride = value; + } + + [PublicAPI] + public Color? BackgroundColor + { + get => Background.ModulateSelfOverride; + set => Background.ModulateSelfOverride = value; + } + + [PublicAPI] + public Thickness InsideMargin + { + get => Foreground.Margin; + set => Foreground.Margin = value; + } + + public AdvancedProgressBar() + { + RobustXamlLoader.Load(this); + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + return Vector2.Zero; + } + + protected override void Draw(DrawingHandleScreen handle) + { + switch (Orientation) + { + default: + case BarOrientation.LeftToRight: + case BarOrientation.RightToLeft: + Foreground.SetWidth = (Width - InsideMargin.SumHorizontal) * Percent; + Foreground.SetHeight = float.NaN; + break; + + case BarOrientation.TopToBottom: + case BarOrientation.BottomToTop: + Foreground.SetWidth = float.NaN; + Foreground.SetHeight = (Height - InsideMargin.SumVertical) * Percent; + break; + } + + base.Draw(handle); + } + + private void BindValues() + { + Background.SetOnlyStyleClass(BackgroundColor.HasValue ? ColorableStyleClass : BackgroundStyleClass); + Foreground.SetOnlyStyleClass(ForegroundColor.HasValue ? ColorableStyleClass : ForegroundStyleClass); + + switch (Orientation) + { + default: + case BarOrientation.LeftToRight: + case BarOrientation.RightToLeft: + Foreground.HorizontalAlignment = Orientation == BarOrientation.RightToLeft ? HAlignment.Right : HAlignment.Left; + Foreground.VerticalAlignment = VAlignment.Stretch; + break; + + case BarOrientation.TopToBottom: + case BarOrientation.BottomToTop: + Foreground.HorizontalAlignment = HAlignment.Stretch; + Foreground.VerticalAlignment = Orientation == BarOrientation.BottomToTop ? VAlignment.Bottom : VAlignment.Top; + break; + } + } +} diff --git a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml new file mode 100644 index 00000000000..a134a35b952 --- /dev/null +++ b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs new file mode 100644 index 00000000000..6b4c8d26b01 --- /dev/null +++ b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs @@ -0,0 +1,244 @@ +using JetBrains.Annotations; +using Robust.Client.Animations; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Animations; + +namespace Content.Client._DV.UserInterfaces.Controls; + +[GenerateTypedNameReferences] +public sealed partial class StatusLight : Control +{ + private const string BlinkingAnimationKey = "blink"; + + private static readonly Color DefaultDimColor = Color.FromHex("#202020"); + + private static readonly Animation BlinkingFastAnimation = new() + { + Length = TimeSpan.FromSeconds(0.2), + AnimationTracks = + { + new AnimationTrackControlProperty + { + Property = nameof(Modulate), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Color.White, 0f), + new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.1f), + new AnimationTrackProperty.KeyFrame(Color.White, 0.1f) + } + } + } + }; + + private static readonly Animation BlinkingSlowAnimation = new() + { + Length = TimeSpan.FromSeconds(0.8), + AnimationTracks = + { + new AnimationTrackControlProperty + { + Property = nameof(Modulate), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Color.White, 0f), + new AnimationTrackProperty.KeyFrame(Color.White, 0.3f), + new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.1f), + new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.3f), + new AnimationTrackProperty.KeyFrame(Color.White, 0.1f), + } + } + } + }; + + public enum BlinkMode : byte + { + /// + /// Blinking disabled. + /// + None, + + /// + /// Blink once per 200ms. + /// + Fast, + + /// + /// Blink once per 800ms. + /// + Slow + } + + /// + /// The blinking mode of the indicator, if any. Defaults to . + /// + [PublicAPI] + public BlinkMode Blinking + { + get; + set + { + if (field == value) return; + + field = value; + BindValues(); + } + } = BlinkMode.None; + + /// + /// Whether the indicator should be lit. + /// + [PublicAPI] + public bool IsOn + { + get; + set + { + if (field == value) return; + + field = value; + BindValues(); + } + } + + /// + /// The color of the indicator when it is on. + /// + [PublicAPI] + public Color Color + { + get; + set + { + if (field == value) return; + + field = value; + BindValues(); + } + } = Color.Green.WithAlpha(0.33f); + + /// + /// The base color of the indicator, also shown when it is off. + /// + [PublicAPI] + public Color? DimColor + { + get; + set + { + if (field == value) return; + + field = value; + BindValues(); + } + } + + /// + /// The used by the Active Light. + /// + [PublicAPI] + public Texture? ActiveLightTexture + { + get => ActiveLight.Texture; + set => ActiveLight.Texture = value; + } + + /// + /// Setter to load a into . + /// + /// + /// Shortcut for the property. + /// + [PublicAPI] + public string ActiveLightTexturePath + { + set => ActiveLight.TexturePath = value; + } + + /// + /// The used by the Base Light. + /// + [PublicAPI] + public Texture? BaseLightTexture + { + get => BaseLight.Texture; + set => BaseLight.Texture = value; + } + + /// + /// Setter to load a into . + /// + /// + /// Shortcut for the property. + /// + [PublicAPI] + public string BaseLightTexturePath + { + set => BaseLight.TexturePath = value; + } + + public StatusLight() + { + RobustXamlLoader.Load(this); + + ActiveLight.AnimationCompleted += OnAnimationCompleted; + + BindValues(); + } + + /// + /// Update the control's visuals based on the current property values. + /// + private void BindValues() + { + BaseLight.ModulateSelfOverride = DimColor ?? DefaultDimColor; + ActiveLight.ModulateSelfOverride = Color; + ActiveLight.Visible = IsOn; + + ResetAnimation(); + } + + /// + /// Reset the blinking animation based on the current mode. + /// + private void ResetAnimation() + { + if (!IsOn) + { + ActiveLight.StopAnimation(BlinkingAnimationKey); + } + else + { + switch (Blinking) + { + case BlinkMode.Fast: + ActiveLight.PlayAnimation(BlinkingFastAnimation, BlinkingAnimationKey); + break; + case BlinkMode.Slow: + ActiveLight.PlayAnimation(BlinkingSlowAnimation, BlinkingAnimationKey); + break; + case BlinkMode.None: + ActiveLight.StopAnimation(BlinkingAnimationKey); + break; + default: + throw new InvalidOperationException($"Unhandled {nameof(BlinkMode)}: {Blinking}"); + } + } + } + + /// + /// Handler for the AnimationCompleted delegate. + /// + /// + private void OnAnimationCompleted(string animationId) + { + if(animationId != BlinkingAnimationKey) return; + + ResetAnimation(); + } +} diff --git a/Content.Server/_DV/Kitchen/DeepFryerSystem.cs b/Content.Server/_DV/Kitchen/DeepFryerSystem.cs index f5a6575aed0..18a524e0c00 100644 --- a/Content.Server/_DV/Kitchen/DeepFryerSystem.cs +++ b/Content.Server/_DV/Kitchen/DeepFryerSystem.cs @@ -1,16 +1,19 @@ using System.Linq; using Content.Shared._DV.Kitchen; +using Content.Shared._DV.Kitchen.BUI; using Content.Shared._DV.Kitchen.Components; using Content.Shared._DV.Kitchen.Systems; using Content.Shared.Audio; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.FixedPoint; +using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; using Content.Shared.Power; using Content.Shared.Power.EntitySystems; using Content.Shared.Throwing; using Content.Shared.Trigger.Systems; +using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -30,6 +33,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedPowerReceiverSystem _power = default!; [Dependency] private readonly TriggerSystem _trigger = default!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; /// /// The trigger key used when non-frying oil reagents are added to the fryer @@ -46,10 +50,23 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem SubscribeLocalEvent(OnItemInserted); SubscribeLocalEvent(OnItemRemoved); + SubscribeLocalEvent(OnDeepFryerUIOpened); SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnSolutionChanged); SubscribeLocalEvent(OnSolutionTransferred); SubscribeLocalEvent(OnThrowHitBy); } + + private void OnSolutionChanged(Entity ent, ref SolutionContainerChangedEvent args) + { + // This event also fires for drinking the oil (which does not count as transferring from the solution) + UpdateUserInterfaceState(ent); + } + + private void OnDeepFryerUIOpened(Entity ent, ref BoundUIOpenedEvent args) + { + UpdateUserInterfaceState(ent); + } private void OnSolutionTransferred(Entity ent, ref SolutionTransferredEvent args) { @@ -90,6 +107,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) { UpdateAppearance(ent); + UpdateUserInterfaceState(ent); } private void OnThrowHitBy(Entity ent, ref ThrowHitByEvent args) @@ -128,6 +146,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem } UpdateAppearance(ent); + UpdateUserInterfaceState(ent); } private void OnItemRemoved(Entity ent, ref EntRemovedFromContainerMessage args) @@ -140,6 +159,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem ent.Comp.CookingItems.Remove(args.Entity); UpdateAppearance(ent); + UpdateUserInterfaceState(ent); } /// @@ -166,6 +186,30 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem _ambientSound.SetAmbience(ent, value); } + private void UpdateUserInterfaceState(Entity ent) + { + if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution)) + return; + + if (_uiSystem.HasUi(ent, DeepFryerUiKey.DeepFryer)) + { + _uiSystem.SetUiState(ent.Owner, DeepFryerUiKey.DeepFryer, new DeepFryerBoundUserInterfaceState + { + IsPowered = _power.IsPowered(ent.Owner), + + OilQuality = ent.Comp.OilQuality, + + CookingItems = [..ent.Comp.CookingItems.Keys.Select(item => EntityManager.GetNetEntity(item))], + Capacity = ent.Comp.MaxItems, + + MinimumVolume = ent.Comp.MinimumOilVolume, + SolutionMaxVolume = solution.MaxVolume, + SolutionVolume = solution.Volume, + SolutionColor = solution.GetColor(_prototype), + }); + } + } + /// /// Finds the best recipe for a single item. /// Prioritizes multi-ingredient recipes (returns null so item waits), then single-ingredient recipes. diff --git a/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs b/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs new file mode 100644 index 00000000000..d1f4f51ca74 --- /dev/null +++ b/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs @@ -0,0 +1,23 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.Serialization; +namespace Content.Shared._DV.Kitchen.BUI; + +[NetSerializable, Serializable] +public sealed class DeepFryerBoundUserInterfaceState : BoundUserInterfaceState +{ + public required NetEntity[] CookingItems { get; set; } + + public float OilQuality { get; set; } + + public FixedPoint2 MinimumVolume { get; set; } + + public FixedPoint2 SolutionVolume { get; set; } + + public FixedPoint2 SolutionMaxVolume { get; set; } + + public int Capacity { get; set; } + + public Color SolutionColor { get; set; } + + public bool IsPowered { get; set; } +} diff --git a/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs b/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs index fba6dfaf5bf..31ee6ce581f 100644 --- a/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs +++ b/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs @@ -27,6 +27,8 @@ public abstract class SharedDeepFryerSystem : EntitySystem public override void Initialize() { base.Initialize(); + + SubscribeLocalEvent(OnUiItemEjected); SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnInteractUsing); @@ -34,7 +36,7 @@ public abstract class SharedDeepFryerSystem : EntitySystem SubscribeLocalEvent>(OnGetAlternativeVerbs); SubscribeLocalEvent(OnExamined); } - + private void OnComponentInit(Entity ent, ref ComponentInit args) { _container.EnsureContainer(ent, ent.Comp.ContainerName); @@ -129,6 +131,17 @@ public abstract class SharedDeepFryerSystem : EntitySystem } } + private void OnUiItemEjected(Entity ent, ref DeepFryerTryEjectItemMessage args) + { + if (!TryGetEntity(args.Item, out var itemEnt)) + return; + + if (!TryGetEntity(args.User, out var userEnt)) + return; + + TryEjectItem(ent, itemEnt.Value, userEnt.Value); + } + /// /// Attempts to eject an item from the deep fryer /// @@ -219,7 +232,7 @@ public abstract class SharedDeepFryerSystem : EntitySystem if (!_container.Insert(item, container)) return false; - Popup.PopupClient(Loc.GetString("deep-fryer-insert-item", ("item", item)), ent, user ?? EntityUid.Invalid); + Popup.PopupClient(Loc.GetString("deep-fryer-insert-item-success", ("item", item)), ent, user ?? EntityUid.Invalid); return true; } @@ -318,3 +331,17 @@ public abstract class SharedDeepFryerSystem : EntitySystem [Serializable, NetSerializable] public sealed partial class DeepFryerInsertDoAfterEvent : SimpleDoAfterEvent; + +[Serializable, NetSerializable] +public sealed class DeepFryerTryEjectItemMessage(NetEntity item, NetEntity? user) : BoundUserInterfaceMessage +{ + public NetEntity Item { get; set; } = item; + + public NetEntity? User { get; set; } = user; +} + +[Serializable, NetSerializable] +public enum DeepFryerUiKey : byte +{ + DeepFryer +} \ No newline at end of file diff --git a/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl b/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl index 9c03be3c3f7..8d2355f00f9 100644 --- a/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl +++ b/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl @@ -4,7 +4,8 @@ deep-fryer-not-food = That's not something you can fry! deep-fryer-no-container = The fryer basket is missing! deep-fryer-full = The fryer is full! deep-fryer-insufficient-oil = There's not enough oil in the fryer! -deep-fryer-insert-item = You insert {THE($item)} into the deep fryer. +deep-fryer-insert-item = Insert {THE($item)} +deep-fryer-insert-item-success = You insert {THE($item)} into the deep fryer. deep-fryer-eject-item = Eject {THE($item)} deep-fryer-eject-item-success = You eject {THE($item)} from the fryer. deep-fryer-item-finished = {CAPITALIZE(THE($item))} has finished cooking! @@ -20,3 +21,10 @@ deep-fryer-oil-quality-used = used deep-fryer-oil-quality-dirty = dirty deep-fryer-oil-quality-foul = foul deep-fryer-oil-quality-unknown = unknown + +## UI Text +deep-fryer-ui-title = Mr. Fry +deep-fryer-ui-power-text = Power +deep-fryer-ui-oil-text = Add Oil +deep-fryer-ui-oil-tooltip = At least {$units}u of oil must be in the fryer. +deep-fryer-ui-empty = Empty Basket diff --git a/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml b/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml index fedcbef983b..90eaeb65ea8 100644 --- a/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml +++ b/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml @@ -124,7 +124,13 @@ - !type:ChangeConstructionNodeBehavior node: machineFrame - type: ApcPowerReceiver - powerLoad: 2000 + powerLoad: 2000 + - type: ActivatableUI + key: enum.DeepFryerUiKey.DeepFryer + - type: UserInterface + interfaces: + enum.DeepFryerUiKey.DeepFryer: + type: DeepFryerBoundUserInterface - type: Machine board: DeepFryerMachineCircuitboard - type: EmptyOnMachineDeconstruct From e564f54a8cafab93af915570707f7041f6c22f21 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 00:35:22 -0400 Subject: [PATCH 2/8] chore: remove unused tooltip supplier --- .../_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs index 7587c86cd37..a5338bf09f1 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs @@ -3,11 +3,8 @@ using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.Graphics; -using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Utility; namespace Content.Client._DV.Kitchen.UI.Controls; @@ -123,19 +120,6 @@ public sealed partial class FryerBaskets : PanelContainer _sprite = _entityManager.System(); _metadataQuery = _entityManager.GetEntityQuery(); - - TooltipSupplier = CreateTooltip; - } - - private Tooltip? CreateTooltip(Control sender) - { - if (ToolTip is null) - return null; - - var tooltip = new Tooltip(); - tooltip.SetMessage(FormattedMessage.FromMarkupPermissive(ToolTip)); - - return tooltip; } private void BindValues() From 711c08f2d8ce5248e2a89e10f8005545e0536005 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 00:53:00 -0400 Subject: [PATCH 3/8] fix: hide tooltips when their content may have changed. --- .../_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs | 1 + Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs index 075dfd78591..048495014d6 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs @@ -69,6 +69,7 @@ public sealed partial class FryerItemButton : ContainerButton private void BindValues() { ToolTip = ItemEntity.HasValue ? Loc.GetString("deep-fryer-eject-item", ("item", ItemEntity)) : null; + HideTooltip(); } /// diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs index 403eb7cd9d1..14e065e1ea9 100644 --- a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs @@ -171,7 +171,7 @@ public sealed partial class DeepFryerWindow : BaseWindow _deepFryer = _entityManager.System(); - BasketControl.TooltipSupplier = BuildOilMeterTooltip; + BasketControl.TooltipSupplier = BuildOilQualityTooltip; CloseButton.OnPressed += _ => Close(); FryerBaskets.OnEmptyItemPressed += () => OnEmptyItemPressed?.Invoke(); @@ -210,7 +210,7 @@ public sealed partial class DeepFryerWindow : BaseWindow private (Color color, string labelName) GetOilQualityInfo() => _deepFryer.GetOilQualityInfo(OilQualityCategory); - private Tooltip? BuildOilMeterTooltip(Control control) + private Tooltip? BuildOilQualityTooltip(Control control) { if (SolutionVolume.Value == 0 || _oilQualityTooltip is null) return null; @@ -240,6 +240,8 @@ public sealed partial class DeepFryerWindow : BaseWindow ("color", color.ToHex()), ("state", labelName))); + BasketControl.HideTooltip(); + FryerBaskets.Contents = CookingItems; FryerBaskets.Capacity = Capacity; } From 0083c14822b2bf3558f3ed11f015c55488dd4c05 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 03:02:38 -0400 Subject: [PATCH 4/8] feat: adjust styling according to general feedback --- .../Kitchen/UI/Controls/FryerBaskets.xaml.cs | 4 ++-- .../Kitchen/UI/Controls/FryerItemButton.xaml | 22 +++++++++++-------- .../UI/Controls/FryerItemButton.xaml.cs | 10 +++++++++ .../_DV/Kitchen/UI/DeepFryerWindow.xaml | 9 ++++---- .../_DV/kitchen/deep-fryer-component.ftl | 2 +- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs index a5338bf09f1..e77c3e14222 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs @@ -142,7 +142,7 @@ public sealed partial class FryerBaskets : PanelContainer { fryerItemButton.ItemEntity = Contents[i]; fryerItemButton.ItemTexture = TryGetTextureForEntity(Contents[i], out var texture) ? texture : null; - + fryerItemButton.Align = BoxContainer.AlignMode.Begin; fryerItemButton.Text = _metadataQuery.TryGetComponent(Contents[i], out var metadata) ? metadata.EntityName : null; fryerItemButton.Visible = true; } @@ -150,7 +150,7 @@ public sealed partial class FryerBaskets : PanelContainer { fryerItemButton.ItemEntity = null; fryerItemButton.ItemTexture = null; - + fryerItemButton.Align = BoxContainer.AlignMode.Center; fryerItemButton.Text = Loc.GetString(EmptyBasketText ?? "deep-fryer-ui-empty"); fryerItemButton.Visible = ShowEmptyBaskets; } diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml index 3a8ad9eff79..66205d53c23 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml @@ -1,18 +1,22 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs index 048495014d6..c308d390caf 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs @@ -53,6 +53,16 @@ public sealed partial class FryerItemButton : ContainerButton get => TextLabel.Text; set => TextLabel.Text = value; } + + /// + /// The alignment of the button's contents along it's orientation axis. + /// + [PublicAPI] + public BoxContainer.AlignMode Align + { + get => Container.Align; + set => Container.Align = value; + } public FryerItemButton() { diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml index adf509ffe45..15ab13061e6 100644 --- a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml @@ -6,8 +6,8 @@ Resizable="True" MouseFilter="Pass" - MinSize="385 360" - SetSize="550 360"> + MinSize="385 310" + SetSize="550 310"> - + diff --git a/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl b/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl index 8d2355f00f9..d402bd58c2e 100644 --- a/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl +++ b/Resources/Locale/en-US/_DV/kitchen/deep-fryer-component.ftl @@ -27,4 +27,4 @@ deep-fryer-ui-title = Mr. Fry deep-fryer-ui-power-text = Power deep-fryer-ui-oil-text = Add Oil deep-fryer-ui-oil-tooltip = At least {$units}u of oil must be in the fryer. -deep-fryer-ui-empty = Empty Basket +deep-fryer-ui-empty = [bold]Empty Basket[/bold] From 441d498c91d4f010f41802efeae8cd62db165127 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 04:35:23 -0400 Subject: [PATCH 5/8] refactor: Set up StatusLight with a whole bunch of sheetlet properties because those seem cool --- .../_DV/Kitchen/UI/DeepFryerWindow.xaml | 10 +- .../Sheetlets/StatusLightSheetlet.cs | 89 ++++++ .../UserInterfaces/Controls/StatusLight.xaml | 8 +- .../Controls/StatusLight.xaml.cs | 254 +++++++++--------- 4 files changed, 222 insertions(+), 139 deletions(-) create mode 100644 Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml index 15ab13061e6..5c0b1835541 100644 --- a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml @@ -24,7 +24,9 @@ Name="PowerIndicator" SetSize="24 24" Margin="10" - Color="#FF000066" + + ActiveColorOverride="#FF000066" + VerticalAlignment="Center" IsOn="True" /> @@ -38,11 +40,13 @@ diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs new file mode 100644 index 00000000000..706b6ab1ba3 --- /dev/null +++ b/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs @@ -0,0 +1,89 @@ +using Content.Client._DV.UserInterfaces.Controls; +using Content.Client.Resources; +using Content.Client.Stylesheets; +using Robust.Client.Animations; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Animations; + +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client._DV.Stylesheets.Sheetlets; + +[CommonSheetlet] +public sealed class StatusLightSheetlet : Sheetlet where T : PalettedStylesheet +{ + private const string DefaultBaseLightResPath = "/Textures/Interface/WireHacking/light_off_base.svg.96dpi.png"; + private const string DefaultActiveLightResPath = "/Textures/Interface/WireHacking/light_on_base.svg.96dpi.png"; + + private readonly Animation _blinkingFastAnimation = new() + { + Length = TimeSpan.FromSeconds(0.2), + AnimationTracks = + { + new AnimationTrackControlProperty + { + Property = nameof(Control.Modulate), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Color.White, 0f), + new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.1f), + new AnimationTrackProperty.KeyFrame(Color.White, 0.1f) + } + } + } + }; + + private readonly Animation _blinkingSlowAnimation = new() + { + Length = TimeSpan.FromSeconds(0.8), + AnimationTracks = + { + new AnimationTrackControlProperty + { + Property = nameof(Control.Modulate), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Color.White, 0f), + new AnimationTrackProperty.KeyFrame(Color.White, 0.3f), + new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.1f), + new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.3f), + new AnimationTrackProperty.KeyFrame(Color.White, 0.1f), + } + } + } + }; + + + public override StyleRule[] GetRules(T sheet, object config) + { + return + [ + E() + .Prop(StatusLight.StylePropertyBlinkingAnimation, false) + .Prop(StatusLight.StylePropertyActiveColor, Color.Green.WithAlpha(0.3f)) + .Prop(StatusLight.StylePropertyBaseColor, Color.FromHex("#202020")), + + E() + .ParentOf(E().Class(StatusLight.StyleClassBaseLight)) + .Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture(DefaultBaseLightResPath)), + + E() + .ParentOf(E().Class(StatusLight.StyleClassActiveLight)) + .Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture(DefaultActiveLightResPath)), + + /* Animation Classes */ + E() + .Class(StatusLight.StyleClassFastBlinking) + .Pseudo(StatusLight.StylePseudoClassIsOn) + .Prop(StatusLight.StylePropertyBlinkingAnimation, _blinkingFastAnimation), + + E() + .Class(StatusLight.StyleClassSlowBlinking) + .Pseudo(StatusLight.StylePseudoClassIsOn) + .Prop(StatusLight.StylePropertyBlinkingAnimation, _blinkingSlowAnimation) + ]; + } +} diff --git a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml index a134a35b952..738039cb903 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml +++ b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml @@ -3,15 +3,19 @@ xmlns:controls="clr-namespace:Content.Client._DV.UserInterfaces.Controls" MouseFilter="Pass"> - /// Blinking disabled. - /// - None, - - /// - /// Blink once per 200ms. - /// - Fast, - - /// - /// Blink once per 800ms. - /// - Slow - } - /// - /// The blinking mode of the indicator, if any. Defaults to . + /// Style Class for the Status Light which sets the blinking animation to On 100ms, Off 100ms /// [PublicAPI] - public BlinkMode Blinking - { - get; - set - { - if (field == value) return; - - field = value; - BindValues(); - } - } = BlinkMode.None; + public const string StyleClassFastBlinking = "BlinkingAnimation:Fast"; + + /// + /// Style Class for the Status Light which sets the blinking animation to On 400ms, Off 400ms + /// + [PublicAPI] + public const string StyleClassSlowBlinking = "BlinkingAnimation:Slow"; + + /// + /// Style Class for targeting this control's Active Light . + /// + [PublicAPI] + public const string StyleClassActiveLight = "StatusLight.ActiveLight"; + + /// + /// Style Class for targeting this control's Base Light . + /// + [PublicAPI] + public const string StyleClassBaseLight = "StatusLight.BaseLight"; + + /// + /// Style Pseudo Class for when the light is on. This is mutually exclusive with . + /// + [PublicAPI] + public const string StylePseudoClassIsOn = ":IsOn"; + + /// + /// Style Pseudo Class for when the light is off. This is mutually exclusive with . + /// + [PublicAPI] + public const string StylePseudoClassIsOff = ":IsOff"; + + /// + /// Style Property for setting the blinking animation. + /// + [PublicAPI] + public const string StylePropertyBlinkingAnimation = "Animation:Blinking"; + + /// + /// Style Property for setting the active color of the light. This is the color shown when the light is on. + /// + [PublicAPI] + public const string StylePropertyActiveColor = "Modulate:Light"; + + /// + /// Style Property for setting the base color of the light. This is the color shown when the light is off. + /// + [PublicAPI] + public const string StylePropertyBaseColor = "Modulate:Base"; /// /// Whether the indicator should be lit. @@ -100,9 +77,9 @@ public sealed partial class StatusLight : Control set { if (field == value) return; - + field = value; - BindValues(); + UpdateStylePseudoClasses(); } } @@ -110,7 +87,7 @@ public sealed partial class StatusLight : Control /// The color of the indicator when it is on. /// [PublicAPI] - public Color Color + public Color? ActiveColorOverride { get; set @@ -118,15 +95,15 @@ public sealed partial class StatusLight : Control if (field == value) return; field = value; - BindValues(); + InvalidateStyleSheet(); } - } = Color.Green.WithAlpha(0.33f); + } /// /// The base color of the indicator, also shown when it is off. /// [PublicAPI] - public Color? DimColor + public Color? BaseColorOverride { get; set @@ -134,7 +111,7 @@ public sealed partial class StatusLight : Control if (field == value) return; field = value; - BindValues(); + InvalidateStyleSheet(); } } @@ -142,92 +119,76 @@ public sealed partial class StatusLight : Control /// The used by the Active Light. /// [PublicAPI] - public Texture? ActiveLightTexture + public Texture? ActiveLightTextureOverride { get => ActiveLight.Texture; set => ActiveLight.Texture = value; } - /// - /// Setter to load a into . - /// - /// - /// Shortcut for the property. - /// - [PublicAPI] - public string ActiveLightTexturePath - { - set => ActiveLight.TexturePath = value; - } - /// /// The used by the Base Light. /// [PublicAPI] - public Texture? BaseLightTexture + public Texture? BaseLightTextureOverride { get => BaseLight.Texture; set => BaseLight.Texture = value; } - + /// - /// Setter to load a into . + /// The blinking animation to use for the Active Light. /// - /// - /// Shortcut for the property. - /// [PublicAPI] - public string BaseLightTexturePath + public Animation? BlinkingAnimationOverride { - set => BaseLight.TexturePath = value; + get; + set + { + if (field == value) return; + + field = value; + InvalidateStyleSheet(); + } } public StatusLight() { RobustXamlLoader.Load(this); - - ActiveLight.AnimationCompleted += OnAnimationCompleted; - - BindValues(); + UpdateStylePseudoClasses(); + ActiveLight.AnimationCompleted += ActiveLightOnAnimationCompleted; } - /// - /// Update the control's visuals based on the current property values. - /// - private void BindValues() + protected override void StylePropertiesChanged() { - BaseLight.ModulateSelfOverride = DimColor ?? DefaultDimColor; - ActiveLight.ModulateSelfOverride = Color; + if (GetActualBaseColor() is var baseColor) BaseLight.ModulateSelfOverride = baseColor; + + if (GetActualActiveColor() is var activeColor) ActiveLight.ModulateSelfOverride = activeColor; + + SetActiveLightAnimation(); + + base.StylePropertiesChanged(); + } + + private void UpdateStylePseudoClasses() + { + SetOnlyStylePseudoClass(IsOn ? StylePseudoClassIsOn : StylePseudoClassIsOff); ActiveLight.Visible = IsOn; - ResetAnimation(); + InvalidateStyleSheet(); } /// - /// Reset the blinking animation based on the current mode. + /// Reset the blinking animation based on the current style and overrides. If there is no blinking animation, stop any currently playing animation. /// - private void ResetAnimation() + private void SetActiveLightAnimation() { - if (!IsOn) + if (GetActualBlinkingAnimation() is not { } blinkingAnimation) { ActiveLight.StopAnimation(BlinkingAnimationKey); } - else + else if (!ActiveLight.HasRunningAnimation(BlinkingAnimationKey)) { - switch (Blinking) - { - case BlinkMode.Fast: - ActiveLight.PlayAnimation(BlinkingFastAnimation, BlinkingAnimationKey); - break; - case BlinkMode.Slow: - ActiveLight.PlayAnimation(BlinkingSlowAnimation, BlinkingAnimationKey); - break; - case BlinkMode.None: - ActiveLight.StopAnimation(BlinkingAnimationKey); - break; - default: - throw new InvalidOperationException($"Unhandled {nameof(BlinkMode)}: {Blinking}"); - } + ActiveLight.PlayAnimation(blinkingAnimation, BlinkingAnimationKey); } } @@ -235,10 +196,35 @@ public sealed partial class StatusLight : Control /// Handler for the AnimationCompleted delegate. /// /// - private void OnAnimationCompleted(string animationId) + private void ActiveLightOnAnimationCompleted(string animationId) { - if(animationId != BlinkingAnimationKey) return; + SetActiveLightAnimation(); + } + + private Animation? GetActualBlinkingAnimation() + { + if (BlinkingAnimationOverride is not null) return BlinkingAnimationOverride; - ResetAnimation(); + if (TryGetStyleProperty(StylePropertyBlinkingAnimation, out var blinkingAnimation)) return blinkingAnimation; + + return null; + } + + private Color GetActualActiveColor() + { + if (ActiveColorOverride is not null) return ActiveColorOverride.Value; + + if (TryGetStyleProperty(StylePropertyActiveColor, out var activeColor)) return activeColor; + + return Color.White; + } + + private Color GetActualBaseColor() + { + if (BaseColorOverride is not null) return BaseColorOverride.Value; + + if (TryGetStyleProperty(StylePropertyBaseColor, out var baseColor)) return baseColor; + + return Color.White; } } From 69d0a317297003d26e5760ea625c1b5080cfc456 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 05:35:41 -0400 Subject: [PATCH 6/8] fix: correct the Style Property helpers for horizontal and vertical alignment being applied to horizontal and vertical expand. --- Content.Client/Stylesheets/StylesheetHelpers.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Content.Client/Stylesheets/StylesheetHelpers.cs b/Content.Client/Stylesheets/StylesheetHelpers.cs index 2cd2ec4e2ba..a5d51fe3323 100644 --- a/Content.Client/Stylesheets/StylesheetHelpers.cs +++ b/Content.Client/Stylesheets/StylesheetHelpers.cs @@ -80,12 +80,14 @@ public static class StylesheetHelpers public static MutableSelector HorizontalAlignment(this MutableSelector selector, Control.HAlignment val) { - return selector.Prop(nameof(Control.HorizontalExpand), val); + // DeltaV - Fix HorizontalAlignment being misapplied to HorizontalExpand + return selector.Prop(nameof(Control.HorizontalAlignment), val); } public static MutableSelector VerticalAlignment(this MutableSelector selector, Control.VAlignment val) { - return selector.Prop(nameof(Control.VerticalExpand), val); + // DeltaV - Fix VerticalAlignment being misapplied to VerticalExpand + return selector.Prop(nameof(Control.VerticalAlignment), val); } public static MutableSelector AlignMode(this MutableSelector selector, Label.AlignMode mode) From f5972aaad734bb5959eaa930e5d583d51d542c20 Mon Sep 17 00:00:00 2001 From: faintspeaker Date: Tue, 2 Jun 2026 05:36:54 -0400 Subject: [PATCH 7/8] refactor: add a bunch of style properties/classes and use more sheetlets for the advanced progress bar --- .../_DV/Kitchen/UI/DeepFryerWindow.xaml.cs | 2 +- .../Sheetlets/AdvancedProgressBarSheetlet.cs | 39 ++- .../Controls/AdvancedProgressBar.xaml | 4 +- .../Controls/AdvancedProgressBar.xaml.cs | 226 +++++++++++++++--- 4 files changed, 221 insertions(+), 50 deletions(-) diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs index 14e065e1ea9..19d1d1e90ae 100644 --- a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs @@ -230,7 +230,7 @@ public sealed partial class DeepFryerWindow : BaseWindow OilMeter.MaximumValue = SolutionMaxVolume.Value; OilMeter.Value = SolutionVolume.Value; - OilMeter.ForegroundColor = SolutionColor.WithAlpha(0.8f); + OilMeter.ForegroundColorOverride = SolutionColor.WithAlpha(0.8f); OilIndicator.IsOn = IsPowered && SolutionVolume < MinimumVolume; OilIndicator.ToolTip = OilIndicator.IsOn ? Loc.GetString("deep-fryer-ui-oil-tooltip", ("units", MinimumVolume)) : null; diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs index 2632125a296..0dfc3fd46f4 100644 --- a/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs +++ b/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs @@ -14,24 +14,41 @@ public sealed class AdvancedProgressBarSheetlet : Sheetlet where T : Palet public override StyleRule[] GetRules(T sheet, object config) { - var bgBox = new StyleBoxFlat(sheet.PrimaryPalette.BackgroundDark); - var fgBox = new StyleBoxFlat(sheet.PrimaryPalette.BackgroundLight); - var colorable = new StyleBoxFlat(Color.White); return [ E() - .ParentOf(E().Class(AdvancedProgressBar.BackgroundStyleClass)) - .Panel(bgBox), - - E() - .ParentOf(E().Class(AdvancedProgressBar.ForegroundStyleClass)) - .Panel(fgBox), + .ParentOf(E()) + .Panel(colorable), E() - .ParentOf(E().Class(AdvancedProgressBar.ColorableStyleClass)) - .Panel(colorable), + .Prop(AdvancedProgressBar.StylePropertyBackgroundColor, sheet.PrimaryPalette.BackgroundDark) + .Prop(AdvancedProgressBar.StylePropertyForegroundColor, sheet.PrimaryPalette.Base), + + E() + .Pseudo(AdvancedProgressBar.StylePseudoClassLeftToRight) + .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) + .HorizontalAlignment(Control.HAlignment.Left) + .VerticalAlignment(Control.VAlignment.Stretch), + + E() + .Pseudo(AdvancedProgressBar.StylePseudoClassRightToLeft) + .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) + .HorizontalAlignment(Control.HAlignment.Right) + .VerticalAlignment(Control.VAlignment.Stretch), + + E() + .Pseudo(AdvancedProgressBar.StylePseudoClassTopToBottom) + .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) + .HorizontalAlignment(Control.HAlignment.Stretch) + .VerticalAlignment(Control.VAlignment.Top), + + E() + .Pseudo(AdvancedProgressBar.StylePseudoClassBottomToTop) + .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) + .HorizontalAlignment(Control.HAlignment.Stretch) + .VerticalAlignment(Control.VAlignment.Bottom), ]; } } diff --git a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml index 59e59479ef4..6254645b8c8 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml +++ b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml @@ -2,7 +2,7 @@ xmlns="https://spacestation14.io" xmlns:dv="clr-namespace:Content.Client._DV.UserInterfaces.Controls"> + StyleClasses="BackgroundPanelContainer" /> + StyleClasses="ForegroundPanelContainer"/> \ No newline at end of file diff --git a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs index 9dd705665e3..6861a9c9350 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs +++ b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs @@ -3,15 +3,97 @@ using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Timing; namespace Content.Client._DV.UserInterfaces.Controls; [GenerateTypedNameReferences] public sealed partial class AdvancedProgressBar : Control { - public const string BackgroundStyleClass = "Background"; - public const string ForegroundStyleClass = "Foreground"; - public const string ColorableStyleClass = "Colorable"; + /// + /// Style Class for targeting this control's Background + /// + [PublicAPI] + public const string StyleClassBackgroundPanelContainer = "BackgroundPanelContainer"; + + /// + /// Style class for targeting this control's Foreground + /// + [PublicAPI] + public const string StyleClassForegroundPanelContainer = "ForegroundPanelContainer"; + + /// + /// Style Property for setting the background color of the progress bar. + /// + [PublicAPI] + public const string StylePropertyBackgroundColor = "Modulate:Background"; + + /// + /// Style Property for setting the foreground color of the progress bar. + /// + [PublicAPI] + public const string StylePropertyForegroundColor = "Modulate:Foreground"; + + /// + /// Style Pseudo Class for the current orientation of the progress bar. + /// + [PublicAPI] + public const string StylePseudoClassLeftToRight = ":LeftToRight"; + + /// + /// Style Pseudo Class for the current orientation of the progress bar. + /// + [PublicAPI] + public const string StylePseudoClassRightToLeft = ":RightToLeft"; + + /// + /// Style Pseudo Class for the current orientation of the progress bar. + /// + [PublicAPI] + public const string StylePseudoClassTopToBottom = ":TopToBottom"; + + /// + /// Style Pseudo Class for the current orientation of the progress bar. + /// + [PublicAPI] + public const string StylePseudoClassBottomToTop = ":BottomToTop"; + + /// + /// Style Pseudo Class for when the progress bar's value is at its minimum. + /// + [PublicAPI] + public const string StylePseudoClassEmpty = ":Empty"; + + /// + /// Style Pseudo Class for when the progress bar's value is at its maximum. + /// + [PublicAPI] + public const string StylePseudoClassFull = ":Full"; + + /// + /// Style Pseudo Class for when the progress bar's percentage is in the range of [1%, 25%). + /// + [PublicAPI] + public const string StylePseudoClassFirstQuartile = ":FirstQuartile"; + + /// + /// Style Pseudo Class for when the progress bar's percentage is in the range of [25%, 50%). + /// + [PublicAPI] + public const string StylePseudoClassSecondQuartile = ":SecondQuartile"; + + /// + /// Style Pseudo Class for when the progress bar's percentage is in the range of [50%, 75%). + /// + [PublicAPI] + public const string StylePseudoClassThirdQuartile = ":ThirdQuartile"; + + /// + /// Style Pseudo Class for when the progress bar's percentage is in the range of [75%, 100%). + /// + [PublicAPI] + public const string StylePseudoClassFourthQuartile = ":FourthQuartile"; public enum BarOrientation : byte { @@ -27,8 +109,10 @@ public sealed partial class AdvancedProgressBar : Control get; set { + if (field == value) return; + field = value; - BindValues(); + UpdateStylePseudoClasses(); } } = BarOrientation.LeftToRight; @@ -38,8 +122,10 @@ public sealed partial class AdvancedProgressBar : Control get; set { + if (field == value) return; + field = value; - BindValues(); + UpdateStylePseudoClasses(); } } = 0; @@ -49,8 +135,10 @@ public sealed partial class AdvancedProgressBar : Control get; set { + if (field == value) return; + field = value; - BindValues(); + UpdateStylePseudoClasses(); } } = 100; @@ -60,26 +148,40 @@ public sealed partial class AdvancedProgressBar : Control get; set { - field = Math.Clamp(value, MinimumValue, MaximumValue); - BindValues(); + if (field == value) return; + + field = value; + UpdateStylePseudoClasses(); } } = 0; [PublicAPI] - public float Percent => MaximumValue == 0 ? 0 : (float) Value / MaximumValue; + public float Percent => MaximumValue == 0 ? 0 : (float) (Value - MinimumValue) / MaximumValue; [PublicAPI] - public Color? ForegroundColor + public Color? ForegroundColorOverride { - get => Foreground.ModulateSelfOverride; - set => Foreground.ModulateSelfOverride = value; + get; + set + { + if (field == value) return; + + field = value; + InvalidateStyleSheet(); + } } [PublicAPI] - public Color? BackgroundColor + public Color? BackgroundColorOverride { - get => Background.ModulateSelfOverride; - set => Background.ModulateSelfOverride = value; + get; + set + { + if (field == value) return; + + field = value; + InvalidateStyleSheet(); + } } [PublicAPI] @@ -92,14 +194,25 @@ public sealed partial class AdvancedProgressBar : Control public AdvancedProgressBar() { RobustXamlLoader.Load(this); + + UpdateStylePseudoClasses(); + } + + protected override void StylePropertiesChanged() + { + Foreground.ModulateSelfOverride = GetActualForegroundColor(); + Background.ModulateSelfOverride = GetActualBackgroundColor(); + + base.StylePropertiesChanged(); } protected override Vector2 MeasureOverride(Vector2 availableSize) { + // Returning zero here makes it so we can reliably use this control's width/height to scale the size of the "bar" return Vector2.Zero; } - - protected override void Draw(DrawingHandleScreen handle) + + protected override void FrameUpdate(FrameEventArgs args) { switch (Orientation) { @@ -116,29 +229,70 @@ public sealed partial class AdvancedProgressBar : Control Foreground.SetHeight = (Height - InsideMargin.SumVertical) * Percent; break; } + + base.FrameUpdate(args); + } + + private void UpdateStylePseudoClasses() + { + var pseudo = Orientation switch + { + BarOrientation.LeftToRight => StylePseudoClassLeftToRight, + BarOrientation.RightToLeft => StylePseudoClassRightToLeft, + BarOrientation.TopToBottom => StylePseudoClassTopToBottom, + BarOrientation.BottomToTop => StylePseudoClassBottomToTop, + _ => throw new ArgumentOutOfRangeException() + }; - base.Draw(handle); + SetOnlyStylePseudoClass(pseudo); + + Value = Math.Clamp(Value, MinimumValue, MaximumValue); + + if (Value == MinimumValue) + { + AddStylePseudoClass(StylePseudoClassEmpty); + } + else if (Value == MaximumValue) + { + AddStylePseudoClass(StylePseudoClassFull); + } + else + { + switch (Percent) + { + case < 0.25f: + AddStylePseudoClass(StylePseudoClassFirstQuartile); + break; + case < 0.5f: + AddStylePseudoClass(StylePseudoClassSecondQuartile); + break; + case < 0.75f: + AddStylePseudoClass(StylePseudoClassThirdQuartile); + break; + default: + AddStylePseudoClass(StylePseudoClassFourthQuartile); + break; + } + } + + InvalidateStyleSheet(); + } + + private Color? GetActualBackgroundColor() + { + if (BackgroundColorOverride is not null) return BackgroundColorOverride.Value; + + if (TryGetStyleProperty(StylePropertyBackgroundColor, out var backgroundColor)) return backgroundColor; + + return null; } - private void BindValues() + private Color? GetActualForegroundColor() { - Background.SetOnlyStyleClass(BackgroundColor.HasValue ? ColorableStyleClass : BackgroundStyleClass); - Foreground.SetOnlyStyleClass(ForegroundColor.HasValue ? ColorableStyleClass : ForegroundStyleClass); + if (ForegroundColorOverride is not null) return ForegroundColorOverride.Value; - switch (Orientation) - { - default: - case BarOrientation.LeftToRight: - case BarOrientation.RightToLeft: - Foreground.HorizontalAlignment = Orientation == BarOrientation.RightToLeft ? HAlignment.Right : HAlignment.Left; - Foreground.VerticalAlignment = VAlignment.Stretch; - break; - - case BarOrientation.TopToBottom: - case BarOrientation.BottomToTop: - Foreground.HorizontalAlignment = HAlignment.Stretch; - Foreground.VerticalAlignment = Orientation == BarOrientation.BottomToTop ? VAlignment.Bottom : VAlignment.Top; - break; - } + if (TryGetStyleProperty(StylePropertyForegroundColor, out var foregroundColor)) return foregroundColor; + + return null; } } From ad7d1e8dd3b5630d5917164fa0df3b81ada6826e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:24:27 +0000 Subject: [PATCH 8/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../BUI/DeepFryerBoundUserInterface.cs | 8 +- .../_DV/Kitchen/UI/Controls/FryerBaskets.xaml | 6 +- .../Kitchen/UI/Controls/FryerBaskets.xaml.cs | 45 ++++++----- .../Kitchen/UI/Controls/FryerItemButton.xaml | 18 ++--- .../UI/Controls/FryerItemButton.xaml.cs | 12 +-- .../_DV/Kitchen/UI/DeepFryerWindow.xaml | 28 +++---- .../_DV/Kitchen/UI/DeepFryerWindow.xaml.cs | 56 +++++++------- .../Sheetlets/AdvancedProgressBarSheetlet.cs | 12 +-- .../Sheetlets/DeepFryerSheetlet.cs | 14 ++-- .../Sheetlets/StatusLightSheetlet.cs | 12 +-- .../Controls/AdvancedProgressBar.xaml | 4 +- .../Controls/AdvancedProgressBar.xaml.cs | 74 +++++++++---------- .../UserInterfaces/Controls/StatusLight.xaml | 16 ++-- .../Controls/StatusLight.xaml.cs | 56 +++++++------- Content.Server/_DV/Kitchen/DeepFryerSystem.cs | 10 +-- .../BUI/DeepFryerBoundUserInterfaceState.cs | 16 ++-- .../Kitchen/Systems/SharedDeepFryerSystem.cs | 10 +-- .../Structures/Machines/deep_fryer.yml | 2 +- 18 files changed, 199 insertions(+), 200 deletions(-) diff --git a/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs b/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs index 0fa18dbc3ac..983de45a979 100644 --- a/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs +++ b/Content.Client/_DV/Kitchen/BUI/DeepFryerBoundUserInterface.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Client._DV.Kitchen.UI; using Content.Shared._DV.Kitchen.BUI; using Content.Shared._DV.Kitchen.Systems; @@ -9,7 +9,7 @@ namespace Content.Client._DV.Kitchen.BUI; public sealed class DeepFryerBoundUserInterface : BoundUserInterface { private DeepFryerWindow? _window; - + [Dependency] private IPlayerManager _player = default!; @@ -33,10 +33,10 @@ public sealed class DeepFryerBoundUserInterface : BoundUserInterface protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); - + if (_window is null || state is not DeepFryerBoundUserInterfaceState deepFryerState) return; - + _window.OilQuality = deepFryerState.OilQuality; _window.SolutionColor = deepFryerState.SolutionColor; _window.MinimumVolume = deepFryerState.MinimumVolume; diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml index af046f6b60f..5bacb465612 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml @@ -1,11 +1,11 @@ - - + - \ No newline at end of file + diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs index e77c3e14222..7f96111cb19 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerBaskets.xaml.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; @@ -12,13 +12,13 @@ namespace Content.Client._DV.Kitchen.UI.Controls; public sealed partial class FryerBaskets : PanelContainer { public const string ContentContainerStyleClass = "FryerBasketsContainer"; - + [Dependency] private readonly IEntityManager _entityManager = default!; - + private readonly SpriteSystem _sprite; - - + + private readonly List _itemControls = []; private readonly EntityQuery _metadataQuery; @@ -27,7 +27,7 @@ public sealed partial class FryerBaskets : PanelContainer /// [PublicAPI] public event Action? OnFoodItemPressed; - + /// /// Event raised when pressing a that does not have an assigned . /// @@ -45,7 +45,7 @@ public sealed partial class FryerBaskets : PanelContainer { if (field == value) return; - + field = value; BindValues(); } @@ -62,7 +62,7 @@ public sealed partial class FryerBaskets : PanelContainer { if (Equals(field, value)) return; - + field = value; BindValues(); } @@ -79,29 +79,29 @@ public sealed partial class FryerBaskets : PanelContainer { if (field == value) return; - + field = value; BindValues(); } } - + /// /// The optional of the text to display in empty fryer item buttons. /// [PublicAPI] - public LocId? EmptyBasketText - { + public LocId? EmptyBasketText + { get; set { if (field == value) return; - + field = value; BindValues(); - } + } } - + /// /// The of the container which holds the fryer item buttons. /// @@ -111,13 +111,13 @@ public sealed partial class FryerBaskets : PanelContainer get => ContentsContainer.Margin; set => ContentsContainer.Margin = value; } - + public FryerBaskets() { RobustXamlLoader.Load(this); - + IoCManager.InjectDependencies(this); - + _sprite = _entityManager.System(); _metadataQuery = _entityManager.GetEntityQuery(); } @@ -125,7 +125,7 @@ public sealed partial class FryerBaskets : PanelContainer private void BindValues() { var expectedButtons = Math.Max(Capacity, Contents.Count); - + while (_itemControls.Count < expectedButtons) { var newBtn = new FryerItemButton(); @@ -137,7 +137,7 @@ public sealed partial class FryerBaskets : PanelContainer for (var i = 0; i < _itemControls.Count; i++) { var fryerItemButton = _itemControls[i]; - + if (i < Contents.Count) { fryerItemButton.ItemEntity = Contents[i]; @@ -156,7 +156,7 @@ public sealed partial class FryerBaskets : PanelContainer } } } - + private void OnItemButtonPressed(FryerItemButton.ButtonPressedEventArgs args) { if(args.IsEmpty) @@ -165,7 +165,7 @@ public sealed partial class FryerBaskets : PanelContainer } else { - OnFoodItemPressed?.Invoke(args.ItemEntity.Value); + OnFoodItemPressed?.Invoke(args.ItemEntity.Value); } } @@ -191,4 +191,3 @@ public sealed partial class FryerBaskets : PanelContainer return false; } } - diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml index 66205d53c23..04f7379653d 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml @@ -1,22 +1,22 @@ - - - - + - + - \ No newline at end of file + diff --git a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs index c308d390caf..86f2edc6899 100644 --- a/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/Controls/FryerItemButton.xaml.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; @@ -16,7 +16,7 @@ public sealed partial class FryerItemButton : ContainerButton /// [PublicAPI] public event Action? OnItemPressed; - + /// /// The of the item associated with this button, if any. /// @@ -28,7 +28,7 @@ public sealed partial class FryerItemButton : ContainerButton { if (field == value) return; - + field = value; BindValues(); } @@ -63,11 +63,11 @@ public sealed partial class FryerItemButton : ContainerButton get => Container.Align; set => Container.Align = value; } - + public FryerItemButton() { RobustXamlLoader.Load(this); - + OnPressed += OnButtonPressed; } @@ -75,7 +75,7 @@ public sealed partial class FryerItemButton : ContainerButton { OnItemPressed?.Invoke(new ButtonPressedEventArgs(this, args.Event, ItemEntity)); } - + private void BindValues() { ToolTip = ItemEntity.HasValue ? Loc.GetString("deep-fryer-eject-item", ("item", ItemEntity)) : null; diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml index 5c0b1835541..ab7b8872504 100644 --- a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml @@ -1,21 +1,21 @@ - - - + @@ -24,9 +24,9 @@ Name="PowerIndicator" SetSize="24 24" Margin="10" - + ActiveColorOverride="#FF000066" - + VerticalAlignment="Center" IsOn="True" /> @@ -43,17 +43,17 @@ StyleClasses="BlinkingAnimation:Slow" SetSize="24 24" Margin="10" - + ActiveColorOverride="#FFFF0066" - + VerticalAlignment="Center" IsOn="True" /> - - @@ -67,7 +67,7 @@ HorizontalExpand="True" Orientation="BottomToTop" InsideMargin="5" /> - + - \ No newline at end of file + diff --git a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs index 19d1d1e90ae..cb3efcb8428 100644 --- a/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs +++ b/Content.Client/_DV/Kitchen/UI/DeepFryerWindow.xaml.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using Content.Client._DV.Kitchen.UI.Controls; using Content.Shared._DV.Kitchen.Components; using Content.Shared._DV.Kitchen.Systems; @@ -16,20 +16,20 @@ namespace Content.Client._DV.Kitchen.UI; public sealed partial class DeepFryerWindow : BaseWindow { private const int DragMarginSize = 7; - + [Dependency] private readonly IEntityManager _entityManager = default!; private readonly SharedDeepFryerSystem _deepFryer; - + private FormattedMessage? _oilQualityTooltip; - + /// /// Event raised when pressing a with an assigned . /// [PublicAPI] public event Action? OnFoodItemPressed; - + /// /// Event raised when pressing a without an assigned . /// @@ -47,7 +47,7 @@ public sealed partial class DeepFryerWindow : BaseWindow { if (MathHelper.CloseTo(field, value)) return; - + field = value; BindValues(); } @@ -64,7 +64,7 @@ public sealed partial class DeepFryerWindow : BaseWindow { if (field == value) return; - + field = value; BindValues(); } @@ -81,7 +81,7 @@ public sealed partial class DeepFryerWindow : BaseWindow { if (field == value) return; - + field = value; BindValues(); } @@ -91,14 +91,14 @@ public sealed partial class DeepFryerWindow : BaseWindow /// The current volume of the deep fryer's oil solution. /// [PublicAPI] - public FixedPoint2 SolutionVolume - { + public FixedPoint2 SolutionVolume + { get; set { if (field == value) return; - + field = value; BindValues(); } @@ -108,18 +108,18 @@ public sealed partial class DeepFryerWindow : BaseWindow /// The maximum volume of the deep fryer's oil solution. /// [PublicAPI] - public FixedPoint2 SolutionMaxVolume + public FixedPoint2 SolutionMaxVolume { get; set { if (field == value) return; - + field = value; BindValues(); } - + } /// @@ -152,31 +152,31 @@ public sealed partial class DeepFryerWindow : BaseWindow BindValues(); } } - + /// /// The entities contained in the deep fryer. /// [PublicAPI] - public IReadOnlyList CookingItems { get; set; } = []; + public IReadOnlyList CookingItems { get; set; } = []; /// /// The level of the oil, as determined by . /// private OilQuality OilQualityCategory => SharedDeepFryerSystem.GetOilQualityLevel(OilQuality); - + public DeepFryerWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _deepFryer = _entityManager.System(); - + BasketControl.TooltipSupplier = BuildOilQualityTooltip; - + CloseButton.OnPressed += _ => Close(); FryerBaskets.OnEmptyItemPressed += () => OnEmptyItemPressed?.Invoke(); FryerBaskets.OnFoodItemPressed += entity => OnFoodItemPressed?.Invoke(entity); - + BindValues(); } @@ -207,27 +207,27 @@ public sealed partial class DeepFryerWindow : BaseWindow return mode; } - + private (Color color, string labelName) GetOilQualityInfo() => _deepFryer.GetOilQualityInfo(OilQualityCategory); - + private Tooltip? BuildOilQualityTooltip(Control control) { if (SolutionVolume.Value == 0 || _oilQualityTooltip is null) return null; - + var tooltip = new Tooltip { Tracking = true }; - + tooltip.SetMessage(_oilQualityTooltip); return tooltip; } - + private void BindValues() { PowerIndicator.IsOn = IsPowered; - + OilMeter.MaximumValue = SolutionMaxVolume.Value; OilMeter.Value = SolutionVolume.Value; OilMeter.ForegroundColorOverride = SolutionColor.WithAlpha(0.8f); @@ -239,9 +239,9 @@ public sealed partial class DeepFryerWindow : BaseWindow _oilQualityTooltip = FormattedMessage.FromMarkupPermissive(Loc.GetString("deep-fryer-oil-quality-examine", ("color", color.ToHex()), ("state", labelName))); - + BasketControl.HideTooltip(); - + FryerBaskets.Contents = CookingItems; FryerBaskets.Capacity = Capacity; } diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs index 0dfc3fd46f4..c0a8757a3dc 100644 --- a/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs +++ b/Content.Client/_DV/Stylesheets/Sheetlets/AdvancedProgressBarSheetlet.cs @@ -1,4 +1,4 @@ -using Content.Client._DV.UserInterfaces.Controls; +using Content.Client._DV.UserInterfaces.Controls; using Content.Client.Stylesheets; using Robust.Client.Graphics; using Robust.Client.UserInterface; @@ -21,29 +21,29 @@ public sealed class AdvancedProgressBarSheetlet : Sheetlet where T : Palet E() .ParentOf(E()) .Panel(colorable), - + E() .Prop(AdvancedProgressBar.StylePropertyBackgroundColor, sheet.PrimaryPalette.BackgroundDark) .Prop(AdvancedProgressBar.StylePropertyForegroundColor, sheet.PrimaryPalette.Base), - + E() .Pseudo(AdvancedProgressBar.StylePseudoClassLeftToRight) .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) .HorizontalAlignment(Control.HAlignment.Left) .VerticalAlignment(Control.VAlignment.Stretch), - + E() .Pseudo(AdvancedProgressBar.StylePseudoClassRightToLeft) .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) .HorizontalAlignment(Control.HAlignment.Right) .VerticalAlignment(Control.VAlignment.Stretch), - + E() .Pseudo(AdvancedProgressBar.StylePseudoClassTopToBottom) .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) .HorizontalAlignment(Control.HAlignment.Stretch) .VerticalAlignment(Control.VAlignment.Top), - + E() .Pseudo(AdvancedProgressBar.StylePseudoClassBottomToTop) .ParentOf(E().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer)) diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs index b3b27faf4e9..7a1104cab4e 100644 --- a/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs +++ b/Content.Client/_DV/Stylesheets/Sheetlets/DeepFryerSheetlet.cs @@ -1,4 +1,4 @@ -using Content.Client._DV.Kitchen.UI.Controls; +using Content.Client._DV.Kitchen.UI.Controls; using Content.Client.Stylesheets; using Content.Client.Stylesheets.SheetletConfigs; using Content.Client.Stylesheets.Sheetlets; @@ -16,27 +16,27 @@ public sealed class DeepFryerSheetlet : Sheetlet where T : PalettedStylesh { var itemWrapperBox = new StyleBoxFlat(sheet.SecondaryPalette.BackgroundDark.WithAlpha(0.5f)); var buttonBox = new StyleBoxFlat(sheet.SecondaryPalette.BackgroundLight.WithAlpha(0.90f)); - + var rules = new List { E() .Panel(itemWrapperBox), - + E() .Class(FryerBaskets.ContentContainerStyleClass) .Prop(BoxContainer.StylePropertySeparation, 5), - + E() .Box(buttonBox), - + E() .ParentOf(E()) .Prop(BoxContainer.StylePropertySeparation, 5), }; - + ButtonSheetlet .MakeButtonRules(rules, sheet.SecondaryPalette, null); - + return rules.ToArray(); } } diff --git a/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs b/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs index 706b6ab1ba3..7d1ea74ad30 100644 --- a/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs +++ b/Content.Client/_DV/Stylesheets/Sheetlets/StatusLightSheetlet.cs @@ -1,4 +1,4 @@ -using Content.Client._DV.UserInterfaces.Controls; +using Content.Client._DV.UserInterfaces.Controls; using Content.Client.Resources; using Content.Client.Stylesheets; using Robust.Client.Animations; @@ -15,7 +15,7 @@ public sealed class StatusLightSheetlet : Sheetlet where T : PalettedStyle { private const string DefaultBaseLightResPath = "/Textures/Interface/WireHacking/light_off_base.svg.96dpi.png"; private const string DefaultActiveLightResPath = "/Textures/Interface/WireHacking/light_on_base.svg.96dpi.png"; - + private readonly Animation _blinkingFastAnimation = new() { Length = TimeSpan.FromSeconds(0.2), @@ -65,21 +65,21 @@ public sealed class StatusLightSheetlet : Sheetlet where T : PalettedStyle .Prop(StatusLight.StylePropertyBlinkingAnimation, false) .Prop(StatusLight.StylePropertyActiveColor, Color.Green.WithAlpha(0.3f)) .Prop(StatusLight.StylePropertyBaseColor, Color.FromHex("#202020")), - + E() .ParentOf(E().Class(StatusLight.StyleClassBaseLight)) .Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture(DefaultBaseLightResPath)), - + E() .ParentOf(E().Class(StatusLight.StyleClassActiveLight)) .Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture(DefaultActiveLightResPath)), - + /* Animation Classes */ E() .Class(StatusLight.StyleClassFastBlinking) .Pseudo(StatusLight.StylePseudoClassIsOn) .Prop(StatusLight.StylePropertyBlinkingAnimation, _blinkingFastAnimation), - + E() .Class(StatusLight.StyleClassSlowBlinking) .Pseudo(StatusLight.StylePseudoClassIsOn) diff --git a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml index 6254645b8c8..3c22b1ff324 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml +++ b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml @@ -1,8 +1,8 @@ - - \ No newline at end of file + diff --git a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs index 6861a9c9350..1c123bf5b65 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs +++ b/Content.Client/_DV/UserInterfaces/Controls/AdvancedProgressBar.xaml.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; @@ -16,85 +16,85 @@ public sealed partial class AdvancedProgressBar : Control /// [PublicAPI] public const string StyleClassBackgroundPanelContainer = "BackgroundPanelContainer"; - + /// /// Style class for targeting this control's Foreground /// [PublicAPI] public const string StyleClassForegroundPanelContainer = "ForegroundPanelContainer"; - + /// /// Style Property for setting the background color of the progress bar. /// [PublicAPI] public const string StylePropertyBackgroundColor = "Modulate:Background"; - + /// /// Style Property for setting the foreground color of the progress bar. /// [PublicAPI] public const string StylePropertyForegroundColor = "Modulate:Foreground"; - + /// /// Style Pseudo Class for the current orientation of the progress bar. /// [PublicAPI] public const string StylePseudoClassLeftToRight = ":LeftToRight"; - + /// /// Style Pseudo Class for the current orientation of the progress bar. /// [PublicAPI] public const string StylePseudoClassRightToLeft = ":RightToLeft"; - + /// /// Style Pseudo Class for the current orientation of the progress bar. /// [PublicAPI] public const string StylePseudoClassTopToBottom = ":TopToBottom"; - + /// /// Style Pseudo Class for the current orientation of the progress bar. /// [PublicAPI] public const string StylePseudoClassBottomToTop = ":BottomToTop"; - + /// /// Style Pseudo Class for when the progress bar's value is at its minimum. /// [PublicAPI] public const string StylePseudoClassEmpty = ":Empty"; - + /// /// Style Pseudo Class for when the progress bar's value is at its maximum. /// [PublicAPI] public const string StylePseudoClassFull = ":Full"; - + /// /// Style Pseudo Class for when the progress bar's percentage is in the range of [1%, 25%). /// [PublicAPI] public const string StylePseudoClassFirstQuartile = ":FirstQuartile"; - + /// /// Style Pseudo Class for when the progress bar's percentage is in the range of [25%, 50%). /// [PublicAPI] public const string StylePseudoClassSecondQuartile = ":SecondQuartile"; - + /// /// Style Pseudo Class for when the progress bar's percentage is in the range of [50%, 75%). /// [PublicAPI] public const string StylePseudoClassThirdQuartile = ":ThirdQuartile"; - + /// /// Style Pseudo Class for when the progress bar's percentage is in the range of [75%, 100%). /// [PublicAPI] public const string StylePseudoClassFourthQuartile = ":FourthQuartile"; - + public enum BarOrientation : byte { RightToLeft, @@ -102,7 +102,7 @@ public sealed partial class AdvancedProgressBar : Control TopToBottom, BottomToTop } - + [PublicAPI] public BarOrientation Orientation { @@ -110,12 +110,12 @@ public sealed partial class AdvancedProgressBar : Control set { if (field == value) return; - + field = value; UpdateStylePseudoClasses(); } } = BarOrientation.LeftToRight; - + [PublicAPI] public int MinimumValue { @@ -123,7 +123,7 @@ public sealed partial class AdvancedProgressBar : Control set { if (field == value) return; - + field = value; UpdateStylePseudoClasses(); } @@ -136,7 +136,7 @@ public sealed partial class AdvancedProgressBar : Control set { if (field == value) return; - + field = value; UpdateStylePseudoClasses(); } @@ -149,12 +149,12 @@ public sealed partial class AdvancedProgressBar : Control set { if (field == value) return; - + field = value; UpdateStylePseudoClasses(); } } = 0; - + [PublicAPI] public float Percent => MaximumValue == 0 ? 0 : (float) (Value - MinimumValue) / MaximumValue; @@ -165,12 +165,12 @@ public sealed partial class AdvancedProgressBar : Control set { if (field == value) return; - + field = value; InvalidateStyleSheet(); } } - + [PublicAPI] public Color? BackgroundColorOverride { @@ -178,7 +178,7 @@ public sealed partial class AdvancedProgressBar : Control set { if (field == value) return; - + field = value; InvalidateStyleSheet(); } @@ -194,7 +194,7 @@ public sealed partial class AdvancedProgressBar : Control public AdvancedProgressBar() { RobustXamlLoader.Load(this); - + UpdateStylePseudoClasses(); } @@ -202,16 +202,16 @@ public sealed partial class AdvancedProgressBar : Control { Foreground.ModulateSelfOverride = GetActualForegroundColor(); Background.ModulateSelfOverride = GetActualBackgroundColor(); - + base.StylePropertiesChanged(); } - + protected override Vector2 MeasureOverride(Vector2 availableSize) { // Returning zero here makes it so we can reliably use this control's width/height to scale the size of the "bar" return Vector2.Zero; } - + protected override void FrameUpdate(FrameEventArgs args) { switch (Orientation) @@ -222,7 +222,7 @@ public sealed partial class AdvancedProgressBar : Control Foreground.SetWidth = (Width - InsideMargin.SumHorizontal) * Percent; Foreground.SetHeight = float.NaN; break; - + case BarOrientation.TopToBottom: case BarOrientation.BottomToTop: Foreground.SetWidth = float.NaN; @@ -232,7 +232,7 @@ public sealed partial class AdvancedProgressBar : Control base.FrameUpdate(args); } - + private void UpdateStylePseudoClasses() { var pseudo = Orientation switch @@ -243,11 +243,11 @@ public sealed partial class AdvancedProgressBar : Control BarOrientation.BottomToTop => StylePseudoClassBottomToTop, _ => throw new ArgumentOutOfRangeException() }; - + SetOnlyStylePseudoClass(pseudo); Value = Math.Clamp(Value, MinimumValue, MaximumValue); - + if (Value == MinimumValue) { AddStylePseudoClass(StylePseudoClassEmpty); @@ -274,16 +274,16 @@ public sealed partial class AdvancedProgressBar : Control break; } } - + InvalidateStyleSheet(); } - + private Color? GetActualBackgroundColor() { if (BackgroundColorOverride is not null) return BackgroundColorOverride.Value; - + if (TryGetStyleProperty(StylePropertyBackgroundColor, out var backgroundColor)) return backgroundColor; - + return null; } diff --git a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml index 738039cb903..f7de5e4faba 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml +++ b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml @@ -1,24 +1,24 @@ - - + - \ No newline at end of file + diff --git a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs index 4765209ef9a..a4913d498a8 100644 --- a/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs +++ b/Content.Client/_DV/UserInterfaces/Controls/StatusLight.xaml.cs @@ -1,4 +1,4 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; using Robust.Client.Animations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; @@ -12,61 +12,61 @@ namespace Content.Client._DV.UserInterfaces.Controls; public sealed partial class StatusLight : Control { private const string BlinkingAnimationKey = "Blinking"; - + /// /// Style Class for the Status Light which sets the blinking animation to On 100ms, Off 100ms /// [PublicAPI] public const string StyleClassFastBlinking = "BlinkingAnimation:Fast"; - + /// /// Style Class for the Status Light which sets the blinking animation to On 400ms, Off 400ms /// [PublicAPI] public const string StyleClassSlowBlinking = "BlinkingAnimation:Slow"; - + /// /// Style Class for targeting this control's Active Light . /// [PublicAPI] public const string StyleClassActiveLight = "StatusLight.ActiveLight"; - + /// /// Style Class for targeting this control's Base Light . /// [PublicAPI] public const string StyleClassBaseLight = "StatusLight.BaseLight"; - + /// /// Style Pseudo Class for when the light is on. This is mutually exclusive with . /// [PublicAPI] public const string StylePseudoClassIsOn = ":IsOn"; - + /// /// Style Pseudo Class for when the light is off. This is mutually exclusive with . /// [PublicAPI] public const string StylePseudoClassIsOff = ":IsOff"; - + /// /// Style Property for setting the blinking animation. /// [PublicAPI] public const string StylePropertyBlinkingAnimation = "Animation:Blinking"; - + /// /// Style Property for setting the active color of the light. This is the color shown when the light is on. /// [PublicAPI] public const string StylePropertyActiveColor = "Modulate:Light"; - + /// /// Style Property for setting the base color of the light. This is the color shown when the light is off. /// [PublicAPI] public const string StylePropertyBaseColor = "Modulate:Base"; - + /// /// Whether the indicator should be lit. /// @@ -77,7 +77,7 @@ public sealed partial class StatusLight : Control set { if (field == value) return; - + field = value; UpdateStylePseudoClasses(); } @@ -124,7 +124,7 @@ public sealed partial class StatusLight : Control get => ActiveLight.Texture; set => ActiveLight.Texture = value; } - + /// /// The used by the Base Light. /// @@ -134,7 +134,7 @@ public sealed partial class StatusLight : Control get => BaseLight.Texture; set => BaseLight.Texture = value; } - + /// /// The blinking animation to use for the Active Light. /// @@ -145,7 +145,7 @@ public sealed partial class StatusLight : Control set { if (field == value) return; - + field = value; InvalidateStyleSheet(); } @@ -161,19 +161,19 @@ public sealed partial class StatusLight : Control protected override void StylePropertiesChanged() { if (GetActualBaseColor() is var baseColor) BaseLight.ModulateSelfOverride = baseColor; - + if (GetActualActiveColor() is var activeColor) ActiveLight.ModulateSelfOverride = activeColor; - + SetActiveLightAnimation(); - + base.StylePropertiesChanged(); } - + private void UpdateStylePseudoClasses() { SetOnlyStylePseudoClass(IsOn ? StylePseudoClassIsOn : StylePseudoClassIsOff); ActiveLight.Visible = IsOn; - + InvalidateStyleSheet(); } @@ -204,27 +204,27 @@ public sealed partial class StatusLight : Control private Animation? GetActualBlinkingAnimation() { if (BlinkingAnimationOverride is not null) return BlinkingAnimationOverride; - + if (TryGetStyleProperty(StylePropertyBlinkingAnimation, out var blinkingAnimation)) return blinkingAnimation; - + return null; } - + private Color GetActualActiveColor() { if (ActiveColorOverride is not null) return ActiveColorOverride.Value; - + if (TryGetStyleProperty(StylePropertyActiveColor, out var activeColor)) return activeColor; - + return Color.White; } - + private Color GetActualBaseColor() { if (BaseColorOverride is not null) return BaseColorOverride.Value; - + if (TryGetStyleProperty(StylePropertyBaseColor, out var baseColor)) return baseColor; - + return Color.White; } } diff --git a/Content.Server/_DV/Kitchen/DeepFryerSystem.cs b/Content.Server/_DV/Kitchen/DeepFryerSystem.cs index 18a524e0c00..542f910d069 100644 --- a/Content.Server/_DV/Kitchen/DeepFryerSystem.cs +++ b/Content.Server/_DV/Kitchen/DeepFryerSystem.cs @@ -56,7 +56,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem SubscribeLocalEvent(OnSolutionTransferred); SubscribeLocalEvent(OnThrowHitBy); } - + private void OnSolutionChanged(Entity ent, ref SolutionContainerChangedEvent args) { // This event also fires for drinking the oil (which does not count as transferring from the solution) @@ -190,18 +190,18 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem { if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution)) return; - + if (_uiSystem.HasUi(ent, DeepFryerUiKey.DeepFryer)) { _uiSystem.SetUiState(ent.Owner, DeepFryerUiKey.DeepFryer, new DeepFryerBoundUserInterfaceState { IsPowered = _power.IsPowered(ent.Owner), - + OilQuality = ent.Comp.OilQuality, - + CookingItems = [..ent.Comp.CookingItems.Keys.Select(item => EntityManager.GetNetEntity(item))], Capacity = ent.Comp.MaxItems, - + MinimumVolume = ent.Comp.MinimumOilVolume, SolutionMaxVolume = solution.MaxVolume, SolutionVolume = solution.Volume, diff --git a/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs b/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs index d1f4f51ca74..5ac664a2940 100644 --- a/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs +++ b/Content.Shared/_DV/Kitchen/BUI/DeepFryerBoundUserInterfaceState.cs @@ -1,4 +1,4 @@ -using Content.Shared.FixedPoint; +using Content.Shared.FixedPoint; using Robust.Shared.Serialization; namespace Content.Shared._DV.Kitchen.BUI; @@ -6,18 +6,18 @@ namespace Content.Shared._DV.Kitchen.BUI; public sealed class DeepFryerBoundUserInterfaceState : BoundUserInterfaceState { public required NetEntity[] CookingItems { get; set; } - + public float OilQuality { get; set; } - + public FixedPoint2 MinimumVolume { get; set; } - + public FixedPoint2 SolutionVolume { get; set; } - + public FixedPoint2 SolutionMaxVolume { get; set; } - + public int Capacity { get; set; } - + public Color SolutionColor { get; set; } - + public bool IsPowered { get; set; } } diff --git a/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs b/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs index 31ee6ce581f..796aff54d0a 100644 --- a/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs +++ b/Content.Shared/_DV/Kitchen/Systems/SharedDeepFryerSystem.cs @@ -27,7 +27,7 @@ public abstract class SharedDeepFryerSystem : EntitySystem public override void Initialize() { base.Initialize(); - + SubscribeLocalEvent(OnUiItemEjected); SubscribeLocalEvent(OnComponentInit); @@ -36,7 +36,7 @@ public abstract class SharedDeepFryerSystem : EntitySystem SubscribeLocalEvent>(OnGetAlternativeVerbs); SubscribeLocalEvent(OnExamined); } - + private void OnComponentInit(Entity ent, ref ComponentInit args) { _container.EnsureContainer(ent, ent.Comp.ContainerName); @@ -135,13 +135,13 @@ public abstract class SharedDeepFryerSystem : EntitySystem { if (!TryGetEntity(args.Item, out var itemEnt)) return; - + if (!TryGetEntity(args.User, out var userEnt)) return; TryEjectItem(ent, itemEnt.Value, userEnt.Value); } - + /// /// Attempts to eject an item from the deep fryer /// @@ -344,4 +344,4 @@ public sealed class DeepFryerTryEjectItemMessage(NetEntity item, NetEntity? user public enum DeepFryerUiKey : byte { DeepFryer -} \ No newline at end of file +} diff --git a/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml b/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml index 90eaeb65ea8..3bf8e75b3b1 100644 --- a/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml +++ b/Resources/Prototypes/_DV/Entities/Structures/Machines/deep_fryer.yml @@ -124,7 +124,7 @@ - !type:ChangeConstructionNodeBehavior node: machineFrame - type: ApcPowerReceiver - powerLoad: 2000 + powerLoad: 2000 - type: ActivatableUI key: enum.DeepFryerUiKey.DeepFryer - type: UserInterface