using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Game.CodexUI.Drag; using Theriapolis.Game.Screens; namespace Theriapolis.Game.CodexUI.Core; /// /// Base class for screens implemented in CodexUI. Owns the input snapshot, /// the root widget, the parchment background tiling, the drag-drop /// controller, and the popover layer. Concrete screens override /// to populate the widget tree. /// /// Implements so the existing ScreenManager /// can treat CodexUI screens identically to Myra-based ones. /// public abstract class CodexScreen : IScreen { protected Game1 Game = null!; protected CodexInput Input { get; } = new(); protected CodexWidget? Root; protected CodexAtlas Atlas { get; private set; } = null!; protected DragDropController DragDrop { get; } = new(); /// The popover layer is painted last so floating panels stay above the page. protected Widgets.CodexHoverPopover? Popover { get; set; } private System.EventHandler? _textInputHandler; private bool _layoutDirty = true; public virtual void Initialize(Game1 game) { Game = game; Atlas = game.CodexAtlas; if (CodexFonts.DisplayLarge is null) throw new System.InvalidOperationException("CodexFonts.LoadAll must be called before any CodexScreen is initialized."); _textInputHandler = (_, e) => { // Filter out non-printable controls so backspace etc. routes via // KeyJustPressed instead of pushing into the text buffer. if (e.Character >= 32 && e.Character != 127) Input.OnTextInput(e.Character); }; game.Window.TextInput += _textInputHandler; Root = BuildRoot(); _layoutDirty = true; } public virtual void Deactivate() { if (_textInputHandler is not null) Game.Window.TextInput -= _textInputHandler; } public virtual void Reactivate() { _layoutDirty = true; } /// Concrete screens build the entire widget tree here. Called once after Initialize. protected abstract CodexWidget BuildRoot(); /// Force a re-measure on next frame (e.g. step changed). public void InvalidateLayout() { Root = BuildRoot(); _layoutDirty = true; } public virtual void Update(GameTime gameTime) { Input.Tick(); if (Root is null) return; if (_layoutDirty) { var vp = Game.GraphicsDevice.Viewport; Root.Measure(new Point(vp.Width, vp.Height)); Root.Arrange(new Rectangle(0, 0, vp.Width, vp.Height)); _layoutDirty = false; } Root.Update(gameTime, Input); Popover?.Update(gameTime, Input); DragDrop.Update(gameTime, Input); } public virtual void Draw(GameTime gameTime, SpriteBatch sb) { Game.GraphicsDevice.Clear(CodexColors.BgDeep); sb.Begin(); DrawBackground(sb); Root?.Draw(sb, gameTime); Popover?.Draw(sb, gameTime); DragDrop.Draw(sb); sb.End(); } /// /// Paint a flat across the full viewport. /// The body widget paints its own lighter /// fill on top so cards (which use the slightly-darker /// ) read clearly against their immediate /// background. We don't tile a parchment-grain texture: a 256-px tile /// produces visible seams, and the procedural radial gradients made /// some areas brighter than the cards on top of them. /// private void DrawBackground(SpriteBatch sb) { var vp = Game.GraphicsDevice.Viewport; sb.Draw(Atlas.Pixel, vp.Bounds, CodexColors.BgDeep); } }