mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-07-28 12:17:12 +02:00
Overlays: Move the structure to Ryujinx/UI/Overlay (also no longer need to cross-pass locales)
This commit is contained in:
parent
076dd9a56a
commit
d6232008d5
15 changed files with 138 additions and 66 deletions
|
@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||
/// GPU synchronization manager.
|
||||
/// </summary>
|
||||
public SynchronizationManager Synchronization { get; }
|
||||
public IOverlayManager OverlayManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Presentation window.
|
||||
|
@ -121,14 +122,18 @@ namespace Ryujinx.Graphics.Gpu
|
|||
/// Creates a new instance of the GPU emulation context.
|
||||
/// </summary>
|
||||
/// <param name="renderer">Host renderer</param>
|
||||
public GpuContext(IRenderer renderer, DirtyHacks hacks)
|
||||
/// <param name="hacks">Enabled dirty hacks</param>
|
||||
/// <param name="overlayManager">Overlay manager for rendering overlays</param>
|
||||
public GpuContext(IRenderer renderer, DirtyHacks hacks, IOverlayManager overlayManager)
|
||||
{
|
||||
Renderer = renderer;
|
||||
|
||||
GPFifo = new GPFifoDevice(this);
|
||||
|
||||
Synchronization = new SynchronizationManager();
|
||||
|
||||
|
||||
OverlayManager = overlayManager;
|
||||
|
||||
Window = new Window(this);
|
||||
|
||||
HostInitalized = new ManualResetEvent(false);
|
||||
|
@ -462,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu
|
|||
RunDeferredActions();
|
||||
|
||||
Renderer.Dispose();
|
||||
|
||||
OverlayManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
54
src/Ryujinx.Graphics.Gpu/IOverlay.cs
Normal file
54
src/Ryujinx.Graphics.Gpu/IOverlay.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for overlay functionality
|
||||
/// </summary>
|
||||
public interface IOverlay : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the overlay
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the overlay is visible
|
||||
/// </summary>
|
||||
bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Opacity of the overlay (0.0 to 1.0)
|
||||
/// </summary>
|
||||
float Opacity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// X position of the overlay
|
||||
/// </summary>
|
||||
float X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Y position of the overlay
|
||||
/// </summary>
|
||||
float Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Z-index for overlay ordering
|
||||
/// </summary>
|
||||
int ZIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay (for animations)
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time elapsed since last update</param>
|
||||
/// <param name="screenSize">Current screen size</param>
|
||||
void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render this overlay
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to</param>
|
||||
void Render(SKCanvas canvas);
|
||||
}
|
||||
}
|
30
src/Ryujinx.Graphics.Gpu/IOverlayManager.cs
Normal file
30
src/Ryujinx.Graphics.Gpu/IOverlayManager.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for overlay management functionality
|
||||
/// </summary>
|
||||
public interface IOverlayManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an overlay to the manager
|
||||
/// </summary>
|
||||
/// <param name="overlay">The overlay to add</param>
|
||||
void AddOverlay(IOverlay overlay);
|
||||
|
||||
/// <summary>
|
||||
/// Update all overlays (for animations)
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time elapsed since last update</param>
|
||||
/// <param name="screenSize">Current screen size</param>
|
||||
void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render all visible overlays
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to</param>
|
||||
void Render(SKCanvas canvas);
|
||||
}
|
||||
}
|
|
@ -1,271 +0,0 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OriginalInputConfig = Ryujinx.Common.Configuration.Hid.InputConfig;
|
||||
using OriginalPlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex;
|
||||
using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendType;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Localization strings for the controller overlay
|
||||
/// </summary>
|
||||
public class ControllerOverlayLocalization
|
||||
{
|
||||
public string TitleText { get; set; } = "Controller Bindings";
|
||||
public string NoControllerText { get; set; } = "No controller assigned";
|
||||
public string KeyboardText { get; set; } = "Keyboard";
|
||||
public string ControllerText { get; set; } = "Controller";
|
||||
public string UnknownText { get; set; } = "Unknown";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controller overlay that shows controller bindings matching the original AXAML design
|
||||
/// </summary>
|
||||
public class ControllerOverlay : Overlay
|
||||
{
|
||||
private const float OverlayWidth = 400;
|
||||
private const float Padding = 24;
|
||||
private const float Spacing = 16;
|
||||
private const float PlayerSpacing = 12;
|
||||
private const float PlayerRowHeight = 32;
|
||||
|
||||
private const float TitleTextSize = 25;
|
||||
private const float PlayerTextSize = 22;
|
||||
|
||||
private float _lifespan = 0f;
|
||||
private ControllerOverlayLocalization _localization;
|
||||
|
||||
public ControllerOverlay(ControllerOverlayLocalization localization) : base("ControllerOverlay")
|
||||
{
|
||||
_localization = localization;
|
||||
CreateBaseElements();
|
||||
}
|
||||
|
||||
private void CreateBaseElements()
|
||||
{
|
||||
// Main background container
|
||||
var background = new RectangleElement(0, 0, OverlayWidth, 200, // Dynamic height will be set later
|
||||
new SKColor(0, 0, 0, 224)) // #E0000000
|
||||
{
|
||||
Name = "Background",
|
||||
CornerRadius = 12,
|
||||
BorderColor = new SKColor(255, 255, 255, 64), // #40FFFFFF
|
||||
BorderWidth = 1
|
||||
};
|
||||
AddElement(background);
|
||||
|
||||
// Title text (will be updated with localized text)
|
||||
var titleText = new TextElement(Padding + 30, Padding, _localization.TitleText, TitleTextSize, SKColors.White)
|
||||
{
|
||||
Name = "TitleText",
|
||||
FontStyle = SKFontStyle.Bold
|
||||
};
|
||||
AddElement(titleText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show controller bindings with localized strings
|
||||
/// </summary>
|
||||
public void ShowControllerBindings(List<OriginalInputConfig> inputConfigs, int durationSeconds)
|
||||
{
|
||||
// Update title text
|
||||
var titleElement = FindElement<TextElement>("TitleText");
|
||||
if (titleElement != null)
|
||||
{
|
||||
titleElement.Text = _localization.TitleText;
|
||||
}
|
||||
|
||||
// Reset lifespan and opacity
|
||||
_lifespan = durationSeconds;
|
||||
|
||||
// Clear existing player bindings
|
||||
ClearPlayerBindings();
|
||||
|
||||
// Group controllers by player index (support all players + handheld)
|
||||
var playerBindings = new Dictionary<OriginalPlayerIndex, List<OriginalInputConfig>>();
|
||||
|
||||
foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Handheld))
|
||||
{
|
||||
if (!playerBindings.ContainsKey(config.PlayerIndex))
|
||||
{
|
||||
playerBindings[config.PlayerIndex] = new List<OriginalInputConfig>();
|
||||
}
|
||||
playerBindings[config.PlayerIndex].Add(config);
|
||||
}
|
||||
|
||||
float currentY = Padding + 40; // After title
|
||||
|
||||
// Add player bindings to UI (support 8 players + handheld)
|
||||
var playerIndices = new[]
|
||||
{
|
||||
OriginalPlayerIndex.Player1, OriginalPlayerIndex.Player2, OriginalPlayerIndex.Player3, OriginalPlayerIndex.Player4,
|
||||
OriginalPlayerIndex.Player5, OriginalPlayerIndex.Player6, OriginalPlayerIndex.Player7, OriginalPlayerIndex.Player8,
|
||||
OriginalPlayerIndex.Handheld
|
||||
};
|
||||
|
||||
for (int i = 0; i < playerIndices.Length; i++)
|
||||
{
|
||||
var playerIndex = playerIndices[i];
|
||||
float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing));
|
||||
|
||||
// Player number with colored background (circular badge)
|
||||
var playerColor = GetPlayerColor(i);
|
||||
var playerBadge = new RectangleElement(Padding, rowY, 24, 20, playerColor)
|
||||
{
|
||||
Name = $"PlayerBadge_{i}",
|
||||
CornerRadius = 12
|
||||
};
|
||||
AddElement(playerBadge);
|
||||
|
||||
// Player number text
|
||||
string playerLabel = playerIndex == OriginalPlayerIndex.Handheld ? "H" : $"P{(int)playerIndex + 1}";
|
||||
var playerLabelElement = new TextElement(Padding + 12, rowY + 2, playerLabel, PlayerTextSize, SKColors.White)
|
||||
{
|
||||
Name = $"PlayerLabel_{i}",
|
||||
FontStyle = SKFontStyle.Bold,
|
||||
TextAlign = SKTextAlign.Center
|
||||
};
|
||||
AddElement(playerLabelElement);
|
||||
|
||||
// Controller info
|
||||
if (playerBindings.ContainsKey(playerIndex))
|
||||
{
|
||||
var controllers = playerBindings[playerIndex];
|
||||
var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList();
|
||||
|
||||
var controllerTextElement = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen
|
||||
{
|
||||
Name = $"ControllerText_{i}",
|
||||
FontStyle = SKFontStyle.Bold
|
||||
};
|
||||
AddElement(controllerTextElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
var noControllerTextElement = new TextElement(Padding + 56, rowY + 2, _localization.NoControllerText, PlayerTextSize, new SKColor(128, 128, 128)) // Gray
|
||||
{
|
||||
Name = $"NoControllerText_{i}",
|
||||
FontStyle = SKFontStyle.Italic
|
||||
};
|
||||
AddElement(noControllerTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total height and update background
|
||||
float totalHeight = Padding + 40 + (playerIndices.Length * (PlayerRowHeight + PlayerSpacing)) + Padding + 20;
|
||||
var background = FindElement<RectangleElement>("Background");
|
||||
if (background != null)
|
||||
{
|
||||
background.Height = totalHeight;
|
||||
}
|
||||
|
||||
// Show the overlay (position will be set by Window class with actual dimensions)
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
private static SKColor GetPlayerColor(int playerIndex)
|
||||
{
|
||||
return playerIndex switch
|
||||
{
|
||||
0 => new SKColor(255, 92, 92), // Red for Player 1
|
||||
1 => new SKColor(54, 162, 235), // Blue for Player 2
|
||||
2 => new SKColor(255, 206, 84), // Yellow for Player 3
|
||||
3 => new SKColor(75, 192, 192), // Green for Player 4
|
||||
4 => new SKColor(153, 102, 255), // Purple for Player 5
|
||||
5 => new SKColor(255, 159, 64), // Orange for Player 6
|
||||
6 => new SKColor(199, 199, 199), // Light Gray for Player 7
|
||||
7 => new SKColor(83, 102, 255), // Indigo for Player 8
|
||||
8 => new SKColor(255, 99, 132), // Pink for Handheld
|
||||
_ => new SKColor(128, 128, 128) // Gray fallback
|
||||
};
|
||||
}
|
||||
|
||||
private string GetControllerDisplayName(OriginalInputConfig config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config.Name))
|
||||
{
|
||||
return config.Backend switch
|
||||
{
|
||||
OriginalInputBackendType.WindowKeyboard => _localization.KeyboardText,
|
||||
OriginalInputBackendType.GamepadSDL2 => _localization.ControllerText,
|
||||
_ => _localization.UnknownText
|
||||
};
|
||||
}
|
||||
|
||||
// Truncate long controller names
|
||||
string name = config.Name;
|
||||
if (name.Length > 25)
|
||||
{
|
||||
name = name.Substring(0, 22) + "...";
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all player bindings
|
||||
/// </summary>
|
||||
private void ClearPlayerBindings()
|
||||
{
|
||||
var elementsToRemove = new List<OverlayElement>();
|
||||
|
||||
foreach (var element in GetElements())
|
||||
{
|
||||
if (element.Name.StartsWith("PlayerBadge_") ||
|
||||
element.Name.StartsWith("PlayerLabel_") ||
|
||||
element.Name.StartsWith("ControllerText_") ||
|
||||
element.Name.StartsWith("NoControllerText_"))
|
||||
{
|
||||
elementsToRemove.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var element in elementsToRemove)
|
||||
{
|
||||
RemoveElement(element);
|
||||
element.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay
|
||||
/// </summary>
|
||||
public override void Update(float deltaTime, SKSize screenSize)
|
||||
{
|
||||
_lifespan -= deltaTime;
|
||||
|
||||
if (_lifespan <= 0)
|
||||
{
|
||||
IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lifespan <= 0.5f)
|
||||
{
|
||||
// Fade out during the last 0.5 seconds
|
||||
Opacity = _lifespan / 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Opacity = 1;
|
||||
}
|
||||
|
||||
// Update position if screen size is provided
|
||||
if (screenSize.Width > 0 && screenSize.Height > 0)
|
||||
{
|
||||
SetPositionToTopRight(screenSize.Width, screenSize.Height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position overlay to top-right matching original AXAML positioning
|
||||
/// </summary>
|
||||
public void SetPositionToTopRight(float screenWidth, float screenHeight)
|
||||
{
|
||||
X = screenWidth - OverlayWidth - 20; // 20px margin from right
|
||||
Y = 50; // 50px margin from top
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Image overlay element
|
||||
/// </summary>
|
||||
public class ImageElement : OverlayElement
|
||||
{
|
||||
private SKBitmap _bitmap;
|
||||
private byte[] _imageData;
|
||||
private string _imagePath;
|
||||
|
||||
public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium;
|
||||
public bool MaintainAspectRatio { get; set; } = true;
|
||||
|
||||
public ImageElement()
|
||||
{
|
||||
}
|
||||
|
||||
public ImageElement(float x, float y, float width, float height, byte[] imageData)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SetImageData(imageData);
|
||||
}
|
||||
|
||||
public ImageElement(float x, float y, float width, float height, string imagePath)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SetImagePath(imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from byte array
|
||||
/// </summary>
|
||||
public void SetImageData(byte[] imageData)
|
||||
{
|
||||
_imageData = imageData;
|
||||
_imagePath = null;
|
||||
LoadBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from file path
|
||||
/// </summary>
|
||||
public void SetImagePath(string imagePath)
|
||||
{
|
||||
_imagePath = imagePath;
|
||||
_imageData = null;
|
||||
LoadBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from existing SKBitmap
|
||||
/// </summary>
|
||||
public void SetBitmap(SKBitmap bitmap)
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = bitmap;
|
||||
_imageData = null;
|
||||
_imagePath = null;
|
||||
}
|
||||
|
||||
private void LoadBitmap()
|
||||
{
|
||||
try
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = null;
|
||||
|
||||
if (_imageData != null)
|
||||
{
|
||||
_bitmap = SKBitmap.Decode(_imageData);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_imagePath))
|
||||
{
|
||||
_bitmap = SKBitmap.Decode(_imagePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Failed to load image: {ex.Message}");
|
||||
_bitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || _bitmap == null || Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
FilterQuality = FilterQuality,
|
||||
Color = SKColors.White.WithAlpha((byte)(255 * effectiveOpacity))
|
||||
};
|
||||
|
||||
var sourceRect = new SKRect(0, 0, _bitmap.Width, _bitmap.Height);
|
||||
var destRect = new SKRect(X, Y, X + Width, Y + Height);
|
||||
|
||||
if (MaintainAspectRatio)
|
||||
{
|
||||
// Calculate aspect ratio preserving destination rectangle
|
||||
float sourceAspect = (float)_bitmap.Width / _bitmap.Height;
|
||||
float destAspect = Width / Height;
|
||||
|
||||
if (sourceAspect > destAspect)
|
||||
{
|
||||
// Source is wider, fit to width
|
||||
float newHeight = Width / sourceAspect;
|
||||
float yOffset = (Height - newHeight) / 2;
|
||||
destRect = new SKRect(X, Y + yOffset, X + Width, Y + yOffset + newHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Source is taller, fit to height
|
||||
float newWidth = Height * sourceAspect;
|
||||
float xOffset = (Width - newWidth) / 2;
|
||||
destRect = new SKRect(X + xOffset, Y, X + xOffset + newWidth, Y + Height);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Base overlay class containing multiple elements
|
||||
/// </summary>
|
||||
public abstract class Overlay : IDisposable
|
||||
{
|
||||
private readonly List<OverlayElement> _elements = new();
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public int ZIndex { get; set; } = 0;
|
||||
|
||||
public Overlay()
|
||||
{
|
||||
}
|
||||
|
||||
public Overlay(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an element to this overlay
|
||||
/// </summary>
|
||||
public void AddElement(OverlayElement element)
|
||||
{
|
||||
_elements.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an element from this overlay
|
||||
/// </summary>
|
||||
public void RemoveElement(OverlayElement element)
|
||||
{
|
||||
_elements.Remove(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all elements
|
||||
/// </summary>
|
||||
public IReadOnlyList<OverlayElement> GetElements()
|
||||
{
|
||||
return _elements.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find element by name
|
||||
/// </summary>
|
||||
public T FindElement<T>(string name) where T : OverlayElement
|
||||
{
|
||||
return _elements.OfType<T>().FirstOrDefault(e => e.Name == name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all elements
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
element.Dispose();
|
||||
}
|
||||
_elements.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay
|
||||
/// </summary>
|
||||
public abstract void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render this overlay
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
if (!IsVisible || Opacity <= 0.0f)
|
||||
return;
|
||||
|
||||
// Save canvas state
|
||||
canvas.Save();
|
||||
|
||||
// Apply overlay position offset
|
||||
if (X != 0 || Y != 0)
|
||||
{
|
||||
canvas.Translate(X, Y);
|
||||
}
|
||||
|
||||
// Render all elements
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
if (element.IsVisible)
|
||||
{
|
||||
element.Render(canvas, Opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore canvas state
|
||||
canvas.Restore();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all overlay elements
|
||||
/// </summary>
|
||||
public abstract class OverlayElement : IDisposable
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Render this element to the canvas
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw on</param>
|
||||
/// <param name="globalOpacity">Global opacity multiplier</param>
|
||||
public abstract void Render(SKCanvas canvas, float globalOpacity = 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a point is within this element's bounds
|
||||
/// </summary>
|
||||
public virtual bool Contains(float x, float y)
|
||||
{
|
||||
return x >= X && x <= X + Width && y >= Y && y <= Y + Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bounds of this element
|
||||
/// </summary>
|
||||
public SKRect GetBounds()
|
||||
{
|
||||
return new SKRect(X, Y, X + Width, Y + Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply opacity to a color
|
||||
/// </summary>
|
||||
protected SKColor ApplyOpacity(SKColor color, float opacity)
|
||||
{
|
||||
return color.WithAlpha((byte)(color.Alpha * opacity));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages multiple overlays and handles rendering
|
||||
/// </summary>
|
||||
public class OverlayManager : IDisposable
|
||||
{
|
||||
private readonly List<Overlay> _overlays = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Add an overlay to the manager
|
||||
/// </summary>
|
||||
public void AddOverlay(Overlay overlay)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overlays.Add(overlay);
|
||||
SortOverlays();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an overlay from the manager
|
||||
/// </summary>
|
||||
public void RemoveOverlay(Overlay overlay)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overlays.Remove(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove overlay by name
|
||||
/// </summary>
|
||||
public void RemoveOverlay(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var overlay = _overlays.FirstOrDefault(o => o.Name == name);
|
||||
if (overlay != null)
|
||||
{
|
||||
_overlays.Remove(overlay);
|
||||
overlay.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find overlay by name
|
||||
/// </summary>
|
||||
public Overlay FindOverlay(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overlays.FirstOrDefault(o => o.Name == name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all overlays
|
||||
/// </summary>
|
||||
public IReadOnlyList<Overlay> GetOverlays()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overlays.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all overlays
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays)
|
||||
{
|
||||
overlay.Dispose();
|
||||
}
|
||||
_overlays.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all overlays (for animations)
|
||||
/// </summary>
|
||||
public void Update(float deltaTime, SKSize screenSize = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays.Where(o => o.IsVisible))
|
||||
{
|
||||
overlay.Update(deltaTime, screenSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render all visible overlays
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays.Where(o => o.IsVisible && o.Opacity > 0.0f))
|
||||
{
|
||||
overlay.Render(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort overlays by Z-index
|
||||
/// </summary>
|
||||
private void SortOverlays()
|
||||
{
|
||||
_overlays.Sort((a, b) => a.ZIndex.CompareTo(b.ZIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show overlay
|
||||
/// </summary>
|
||||
public void ShowOverlay(string name)
|
||||
{
|
||||
var overlay = FindOverlay(name);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.IsVisible = true;
|
||||
overlay.Opacity = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide overlay
|
||||
/// </summary>
|
||||
public void HideOverlay(string name)
|
||||
{
|
||||
var overlay = FindOverlay(name);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.IsVisible = false;
|
||||
overlay.Opacity = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
using SkiaSharp;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Rectangle overlay element
|
||||
/// </summary>
|
||||
public class RectangleElement : OverlayElement
|
||||
{
|
||||
public SKColor BackgroundColor { get; set; } = SKColors.Transparent;
|
||||
public SKColor BorderColor { get; set; } = SKColors.Transparent;
|
||||
public float BorderWidth { get; set; } = 0;
|
||||
public float CornerRadius { get; set; } = 0;
|
||||
|
||||
public RectangleElement()
|
||||
{
|
||||
}
|
||||
|
||||
public RectangleElement(float x, float y, float width, float height, SKColor backgroundColor)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
BackgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
var bounds = new SKRect(X, Y, X + Width, Y + Height);
|
||||
|
||||
// Draw background
|
||||
if (BackgroundColor.Alpha > 0)
|
||||
{
|
||||
using var backgroundPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(BackgroundColor, effectiveOpacity),
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, backgroundPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.DrawRect(bounds, backgroundPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw border
|
||||
if (BorderWidth > 0 && BorderColor.Alpha > 0)
|
||||
{
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(BorderColor, effectiveOpacity),
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = BorderWidth,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, borderPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.DrawRect(bounds, borderPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
using SkiaSharp;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Text overlay element
|
||||
/// </summary>
|
||||
public class TextElement : OverlayElement
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public SKColor TextColor { get; set; } = SKColors.White;
|
||||
public float FontSize { get; set; } = 16;
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
public SKFontStyle FontStyle { get; set; } = SKFontStyle.Normal;
|
||||
public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left;
|
||||
public bool IsAntialias { get; set; } = true;
|
||||
|
||||
// Shadow properties
|
||||
public bool HasShadow { get; set; } = false;
|
||||
public SKColor ShadowColor { get; set; } = SKColors.Black;
|
||||
public float ShadowOffsetX { get; set; } = 1;
|
||||
public float ShadowOffsetY { get; set; } = 1;
|
||||
public float ShadowBlur { get; set; } = 0;
|
||||
|
||||
public TextElement()
|
||||
{
|
||||
}
|
||||
|
||||
public TextElement(float x, float y, string text, float fontSize = 16, SKColor? color = null)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Text = text;
|
||||
FontSize = fontSize;
|
||||
TextColor = color ?? SKColors.White;
|
||||
|
||||
// Auto-calculate width and height based on text
|
||||
UpdateDimensions();
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || string.IsNullOrEmpty(Text))
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
|
||||
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(TextColor, effectiveOpacity),
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface,
|
||||
TextAlign = TextAlign,
|
||||
IsAntialias = IsAntialias
|
||||
};
|
||||
|
||||
float textX = X;
|
||||
float textY = Y + FontSize; // Baseline adjustment
|
||||
|
||||
// Adjust X position based on alignment
|
||||
if (TextAlign == SKTextAlign.Center)
|
||||
{
|
||||
textX += Width / 2;
|
||||
}
|
||||
else if (TextAlign == SKTextAlign.Right)
|
||||
{
|
||||
textX += Width;
|
||||
}
|
||||
|
||||
// Draw shadow if enabled
|
||||
if (HasShadow)
|
||||
{
|
||||
using var shadowPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(ShadowColor, effectiveOpacity),
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface,
|
||||
TextAlign = TextAlign,
|
||||
IsAntialias = IsAntialias
|
||||
};
|
||||
|
||||
if (ShadowBlur > 0)
|
||||
{
|
||||
shadowPaint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlur);
|
||||
}
|
||||
|
||||
canvas.DrawText(Text, textX + ShadowOffsetX, textY + ShadowOffsetY, shadowPaint);
|
||||
}
|
||||
|
||||
// Draw main text
|
||||
canvas.DrawText(Text, textX, textY, paint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update width and height based on current text and font settings
|
||||
/// </summary>
|
||||
public void UpdateDimensions()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text))
|
||||
{
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface
|
||||
};
|
||||
|
||||
var bounds = new SKRect();
|
||||
paint.MeasureText(Text, ref bounds);
|
||||
|
||||
Width = bounds.Width;
|
||||
Height = FontSize; // Use font size as height for consistency
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ using Ryujinx.Common.Memory;
|
|||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
using Ryujinx.Graphics.Gpu.Memory;
|
||||
using Ryujinx.Graphics.Gpu.Overlay;
|
||||
using Ryujinx.Graphics.Texture;
|
||||
using Ryujinx.Memory.Range;
|
||||
using SkiaSharp;
|
||||
|
@ -20,7 +19,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||
public class Window
|
||||
{
|
||||
private readonly GpuContext _context;
|
||||
private readonly OverlayManager _overlayManager;
|
||||
private DateTime? _lastUpdateTime = null;
|
||||
|
||||
/// <summary>
|
||||
|
@ -105,7 +103,6 @@ namespace Ryujinx.Graphics.Gpu
|
|||
public Window(GpuContext context)
|
||||
{
|
||||
_context = context;
|
||||
_overlayManager = new OverlayManager();
|
||||
|
||||
_frameQueue = new ConcurrentQueue<PresentationTexture>();
|
||||
}
|
||||
|
@ -258,9 +255,9 @@ namespace Ryujinx.Graphics.Gpu
|
|||
/// <summary>
|
||||
/// Add overlay to the overlay manager
|
||||
/// </summary>
|
||||
public void AddOverlay(Overlay.Overlay overlay)
|
||||
public void AddOverlay(IOverlay overlay)
|
||||
{
|
||||
_overlayManager.AddOverlay(overlay);
|
||||
_context.OverlayManager.AddOverlay(overlay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -276,7 +273,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||
{
|
||||
// Calculate delta time for lifespan updates
|
||||
float deltaTime = (float)(currentTime - _lastUpdateTime.Value).TotalSeconds;
|
||||
_overlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height));
|
||||
_context.OverlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height));
|
||||
}
|
||||
|
||||
// Update overlay animations
|
||||
|
@ -326,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||
}
|
||||
|
||||
// Render all overlays
|
||||
_overlayManager.Render(canvas);
|
||||
_context.OverlayManager.Render(canvas);
|
||||
|
||||
// Copy modified bitmap data back to texture data array
|
||||
var pixels = bitmap.Bytes;
|
||||
|
@ -370,13 +367,5 @@ namespace Ryujinx.Graphics.Gpu
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_overlayManager?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue