mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-25 11:14:26 +08:00
feat.airapp与融合桌面
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentIcons.Common;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public static class ComponentCategoryIconResolver
|
||||
{
|
||||
public static Icon ResolveCategoryIcon(
|
||||
string categoryId,
|
||||
IEnumerable<DesktopComponentDefinition> categoryComponents)
|
||||
{
|
||||
if (string.Equals(categoryId, "all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Icon.Apps;
|
||||
}
|
||||
|
||||
var firstComponent = categoryComponents.FirstOrDefault();
|
||||
if (firstComponent is null || string.IsNullOrWhiteSpace(firstComponent.IconKey))
|
||||
{
|
||||
return Icon.Apps;
|
||||
}
|
||||
|
||||
if (Enum.TryParse<Icon>(firstComponent.IconKey, ignoreCase: true, out var icon))
|
||||
{
|
||||
return icon;
|
||||
}
|
||||
|
||||
return Icon.Apps;
|
||||
}
|
||||
}
|
||||
@@ -107,4 +107,32 @@
|
||||
<Exec Command="powershell -ExecutionPolicy Bypass -File "$(MSBuildProjectDirectory)\..\scripts\Generate-VersionFile.ps1" -OutputPath "$(VersionFilePath)" -Version "$(AppVersion)" -Codename "$(AppCodename)"" Condition="'$(OS)' == 'Windows_NT'" />
|
||||
<Exec Command="pwsh -ExecutionPolicy Bypass -File "$(MSBuildProjectDirectory)\..\scripts\Generate-VersionFile.ps1" -OutputPath "$(VersionFilePath)" -Version "$(AppVersion)" -Codename "$(AppCodename)"" Condition="'$(OS)' != 'Windows_NT'" />
|
||||
</Target>
|
||||
<Target Name="BuildAirAppHostOutput"
|
||||
AfterTargets="Build"
|
||||
Condition="'$(BuildingAirAppHost)' != 'true' and '$(SkipAirAppHostBuild)' != 'true'">
|
||||
<Exec Command="dotnet build "$(MSBuildProjectDirectory)\..\LanMountainDesktop.AirAppHost\LanMountainDesktop.AirAppHost.csproj" -c "$(Configuration)" --no-restore -p:BuildProjectReferences=false -p:BuildingAirAppHost=true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CopyAirAppHostOutput"
|
||||
AfterTargets="Build"
|
||||
DependsOnTargets="BuildAirAppHostOutput"
|
||||
Condition="'$(SkipAirAppHostBuild)' != 'true'">
|
||||
<ItemGroup>
|
||||
<_AirAppHostOutput Include="..\LanMountainDesktop.AirAppHost\bin\$(Configuration)\$(TargetFramework)\**\*" />
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(OutDir)AirAppHost" />
|
||||
<Copy SourceFiles="@(_AirAppHostOutput)"
|
||||
DestinationFiles="@(_AirAppHostOutput->'$(OutDir)AirAppHost\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CopyAirAppHostPublishOutput" AfterTargets="Publish" Condition="'$(PublishDir)' != ''">
|
||||
<ItemGroup>
|
||||
<_AirAppHostPublishOutput Include="..\LanMountainDesktop.AirAppHost\bin\$(Configuration)\$(TargetFramework)\**\*" />
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(PublishDir)AirAppHost" />
|
||||
<Copy SourceFiles="@(_AirAppHostPublishOutput)"
|
||||
DestinationFiles="@(_AirAppHostPublishOutput->'$(PublishDir)AirAppHost\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
160
LanMountainDesktop/Services/AirAppLauncherService.cs
Normal file
160
LanMountainDesktop/Services/AirAppLauncherService.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public interface IAirAppLauncherService
|
||||
{
|
||||
void OpenWorldClock(string? sourcePlacementId);
|
||||
|
||||
void OpenWhiteboard(string componentId, string? sourcePlacementId);
|
||||
}
|
||||
|
||||
internal sealed class AirAppLauncherService : IAirAppLauncherService
|
||||
{
|
||||
public const string WorldClockAppId = "world-clock";
|
||||
public const string WhiteboardAppId = "whiteboard";
|
||||
|
||||
private const int LauncherIpcRetryCount = 4;
|
||||
|
||||
public void OpenWorldClock(string? sourcePlacementId)
|
||||
{
|
||||
_ = OpenAsync(WorldClockAppId, BuiltInComponentIds.DesktopWorldClock, sourcePlacementId);
|
||||
}
|
||||
|
||||
public void OpenWhiteboard(string componentId, string? sourcePlacementId)
|
||||
{
|
||||
_ = OpenAsync(WhiteboardAppId, componentId, sourcePlacementId);
|
||||
}
|
||||
|
||||
internal static AirAppOpenRequest BuildOpenRequest(
|
||||
string appId,
|
||||
string? sourceComponentId,
|
||||
string? sourcePlacementId,
|
||||
int requesterProcessId)
|
||||
{
|
||||
return new AirAppOpenRequest(
|
||||
appId.Trim(),
|
||||
string.IsNullOrWhiteSpace(sourceComponentId) ? null : sourceComponentId.Trim(),
|
||||
string.IsNullOrWhiteSpace(sourcePlacementId) ? null : sourcePlacementId.Trim(),
|
||||
requesterProcessId);
|
||||
}
|
||||
|
||||
internal static string BuildSingleInstanceKey(string appId, string? sourceComponentId, string? sourcePlacementId)
|
||||
{
|
||||
var normalizedAppId = string.IsNullOrWhiteSpace(appId) ? "unknown" : appId.Trim();
|
||||
var normalizedComponentId = string.IsNullOrWhiteSpace(sourceComponentId) ? "none" : sourceComponentId.Trim();
|
||||
var normalizedPlacementId = string.IsNullOrWhiteSpace(sourcePlacementId) ? "none" : sourcePlacementId.Trim();
|
||||
return $"{normalizedAppId}:{normalizedComponentId}:{normalizedPlacementId}";
|
||||
}
|
||||
|
||||
private static async Task OpenAsync(string appId, string sourceComponentId, string? sourcePlacementId)
|
||||
{
|
||||
var request = BuildOpenRequest(appId, sourceComponentId, sourcePlacementId, Environment.ProcessId);
|
||||
try
|
||||
{
|
||||
var result = await SendOpenRequestAsync(request).ConfigureAwait(false);
|
||||
if (result.Accepted)
|
||||
{
|
||||
AppLogger.Info("AirAppLauncher", $"Launcher accepted Air APP request. AppId='{appId}'; Code='{result.Code}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Warn("AirAppLauncher", $"Launcher rejected Air APP request. AppId='{appId}'; Code='{result.Code}'; Message='{result.Message}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("AirAppLauncher", $"Failed to open Air APP through Launcher. AppId='{appId}'.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<AirAppOperationResult> SendOpenRequestAsync(AirAppOpenRequest request)
|
||||
{
|
||||
Exception? lastException = null;
|
||||
for (var attempt = 1; attempt <= LauncherIpcRetryCount; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new LanMountainDesktopIpcClient();
|
||||
await client.ConnectAsync(IpcConstants.AirAppLifecyclePipeName).ConfigureAwait(false);
|
||||
var proxy = client.CreateProxy<IAirAppLifecycleService>();
|
||||
return await proxy.OpenAsync(request).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
if (attempt == 1)
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"AirAppLauncher",
|
||||
$"Air APP lifecycle IPC unavailable on first attempt. Pipe='{IpcConstants.AirAppLifecyclePipeName}'. Starting Launcher broker.",
|
||||
ex);
|
||||
TryStartLauncher();
|
||||
}
|
||||
|
||||
await Task.Delay(250 * attempt).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Launcher Air APP IPC is unavailable. Pipe='{IpcConstants.AirAppLifecyclePipeName}'.",
|
||||
lastException);
|
||||
}
|
||||
|
||||
internal static ProcessStartInfo CreateBrokerStartInfo(string launcherPath, int requesterProcessId)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = launcherPath,
|
||||
WorkingDirectory = Path.GetDirectoryName(launcherPath) ?? AppContext.BaseDirectory,
|
||||
UseShellExecute = false
|
||||
};
|
||||
startInfo.ArgumentList.Add("air-app-broker");
|
||||
startInfo.ArgumentList.Add("--requester-pid");
|
||||
startInfo.ArgumentList.Add(requesterProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
private static void TryStartLauncher()
|
||||
{
|
||||
try
|
||||
{
|
||||
var launcherPath = LauncherPathResolver.ResolveLauncherExecutablePath();
|
||||
if (string.IsNullOrWhiteSpace(launcherPath) || !File.Exists(launcherPath))
|
||||
{
|
||||
AppLogger.Warn("AirAppLauncher", "Unable to start Launcher for Air APP request: launcher path was not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var startInfo = CreateBrokerStartInfo(launcherPath, Environment.ProcessId);
|
||||
_ = Process.Start(startInfo);
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"Started Launcher Air APP broker. Path='{launcherPath}'; Pipe='{IpcConstants.AirAppLifecyclePipeName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("AirAppLauncher", "Failed to start Launcher for Air APP request.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AirAppLauncherServiceProvider
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static IAirAppLauncherService? _instance;
|
||||
|
||||
public static IAirAppLauncherService GetOrCreate()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_instance ??= new AirAppLauncherService();
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,8 @@ internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
|
||||
{
|
||||
existingWindow.Show();
|
||||
}
|
||||
|
||||
existingWindow.RefreshDesktopLayer();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -136,6 +138,7 @@ internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
|
||||
_widgetWindows[placement.PlacementId] = window;
|
||||
window.Show();
|
||||
window.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
|
||||
window.RefreshDesktopLayer();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class ComponentLibraryCategoryViewModel
|
||||
public ComponentLibraryCategoryViewModel(
|
||||
string id,
|
||||
string title,
|
||||
Symbol icon,
|
||||
Icon icon,
|
||||
IReadOnlyList<ComponentLibraryItemViewModel> components)
|
||||
{
|
||||
Id = id;
|
||||
@@ -46,7 +46,7 @@ public sealed class ComponentLibraryCategoryViewModel
|
||||
|
||||
public string Title { get; }
|
||||
|
||||
public Symbol Icon { get; }
|
||||
public Icon Icon { get; }
|
||||
|
||||
public IReadOnlyList<ComponentLibraryItemViewModel> Components { get; }
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ public partial class ComponentLibraryWindow : Window
|
||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||
category.Id,
|
||||
GetLocalizedCategoryTitle(category.Id),
|
||||
ResolveCategoryIcon(category.Id),
|
||||
ComponentCategoryIconResolver.ResolveCategoryIcon(
|
||||
category.Id,
|
||||
_componentLibraryService.GetDefinitions().Where(d => string.Equals(d.Category, category.Id, StringComparison.OrdinalIgnoreCase))),
|
||||
itemModels));
|
||||
}
|
||||
|
||||
@@ -176,50 +178,6 @@ public partial class ComponentLibraryWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private Symbol ResolveCategoryIcon(string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Clock;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.CalendarDate;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.WeatherSunny;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Edit;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Play;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Info;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Calculator;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Hourglass;
|
||||
}
|
||||
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
private string GetLocalizedCategoryTitle(string categoryId)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
@@ -7,6 +7,11 @@ public interface IDesktopComponentWidget
|
||||
void ApplyCellSize(double cellSize);
|
||||
}
|
||||
|
||||
public interface IDesktopComponentLifecycleWidget
|
||||
{
|
||||
void OnWidgetDestroyed();
|
||||
}
|
||||
|
||||
public interface ITimeZoneAwareComponentWidget
|
||||
{
|
||||
void SetTimeZoneService(TimeZoneService timeZoneService);
|
||||
|
||||
@@ -7,6 +7,47 @@ using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
internal readonly record struct WeatherSceneProfile(
|
||||
string StyleId,
|
||||
MaterialWeatherCondition Condition,
|
||||
string RendererId,
|
||||
string WeatherLayerId,
|
||||
bool IsNight,
|
||||
bool IsLive)
|
||||
{
|
||||
public string Signature => $"{RendererId}:{WeatherLayerId}:{(IsNight ? "night" : "day")}:{(IsLive ? "live" : "still")}";
|
||||
}
|
||||
|
||||
internal static class WeatherSceneProfileResolver
|
||||
{
|
||||
public static WeatherSceneProfile Resolve(string? styleId, MaterialWeatherCondition condition, bool isNight, bool isLive)
|
||||
{
|
||||
var normalized = WeatherVisualStyleCatalog.Normalize(styleId);
|
||||
var rendererId = normalized switch
|
||||
{
|
||||
WeatherVisualStyleId.Geometric => "geometric",
|
||||
WeatherVisualStyleId.Breezy => "breezy",
|
||||
WeatherVisualStyleId.LemonFlutter => "lemon",
|
||||
_ => "google"
|
||||
};
|
||||
|
||||
var layerId = condition switch
|
||||
{
|
||||
MaterialWeatherCondition.Clear => "clear",
|
||||
MaterialWeatherCondition.PartlyCloudy => "partly-cloudy",
|
||||
MaterialWeatherCondition.Cloudy => "cloudy",
|
||||
MaterialWeatherCondition.Rain => "rain",
|
||||
MaterialWeatherCondition.Storm => "storm",
|
||||
MaterialWeatherCondition.Snow => "snow",
|
||||
MaterialWeatherCondition.Fog => "fog",
|
||||
MaterialWeatherCondition.Haze => "haze",
|
||||
_ => "ambient"
|
||||
};
|
||||
|
||||
return new WeatherSceneProfile(normalized, condition, rendererId, layerId, isNight, isLive);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MaterialWeatherSceneControl : Control
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(66) };
|
||||
@@ -16,32 +57,37 @@ public sealed class MaterialWeatherSceneControl : Control
|
||||
private double _phase;
|
||||
private bool _isLive;
|
||||
private bool _isAttached;
|
||||
|
||||
private static readonly Random _rng = new(42);
|
||||
private bool _isNight;
|
||||
|
||||
public MaterialWeatherSceneControl()
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
_timer.Tick += (_, _) =>
|
||||
{
|
||||
_phase = (_phase + 0.008) % 1d;
|
||||
_phase = (_phase + 0.0065) % 1d;
|
||||
InvalidateVisual();
|
||||
};
|
||||
}
|
||||
|
||||
public void Apply(string? styleId, MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)
|
||||
public void Apply(string? styleId, MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive, bool isNight)
|
||||
{
|
||||
_styleId = WeatherVisualStyleCatalog.Normalize(styleId);
|
||||
_condition = condition;
|
||||
_palette = palette;
|
||||
_isLive = isLive;
|
||||
_isNight = isNight;
|
||||
UpdateTimer();
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
public void Apply(string? styleId, MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)
|
||||
{
|
||||
Apply(styleId, condition, palette, isLive, EstimateNightFromPalette(palette));
|
||||
}
|
||||
|
||||
public void Apply(MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)
|
||||
{
|
||||
Apply(_styleId, condition, palette, isLive);
|
||||
Apply(_styleId, condition, palette, isLive, EstimateNightFromPalette(palette));
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
@@ -63,26 +109,29 @@ public sealed class MaterialWeatherSceneControl : Control
|
||||
base.Render(context);
|
||||
|
||||
var rect = new Rect(Bounds.Size);
|
||||
if (rect.Width <= 1 || rect.Height <= 1) return;
|
||||
if (rect.Width <= 1 || rect.Height <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = WeatherSceneProfileResolver.Resolve(_styleId, _condition, _isNight, _isLive);
|
||||
context.DrawRectangle(CreateLinearBrush(_palette.BackgroundTop, _palette.BackgroundBottom, 0, 0, 1, 1), null, rect);
|
||||
|
||||
using (context.PushClip(rect))
|
||||
{
|
||||
DrawStyleDecoration(context, rect);
|
||||
|
||||
switch (_condition)
|
||||
switch (profile.RendererId)
|
||||
{
|
||||
case MaterialWeatherCondition.Rain:
|
||||
case MaterialWeatherCondition.Storm:
|
||||
DrawRain(context, rect, _condition == MaterialWeatherCondition.Storm);
|
||||
case "geometric":
|
||||
RenderGeometricScene(context, rect, profile);
|
||||
break;
|
||||
case MaterialWeatherCondition.Snow:
|
||||
DrawSnow(context, rect);
|
||||
case "breezy":
|
||||
RenderBreezyScene(context, rect, profile);
|
||||
break;
|
||||
case MaterialWeatherCondition.Fog:
|
||||
case MaterialWeatherCondition.Haze:
|
||||
DrawFog(context, rect);
|
||||
case "lemon":
|
||||
RenderLemonScene(context, rect, profile);
|
||||
break;
|
||||
default:
|
||||
RenderGoogleScene(context, rect, profile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -90,287 +139,537 @@ public sealed class MaterialWeatherSceneControl : Control
|
||||
|
||||
private void UpdateTimer()
|
||||
{
|
||||
if (_isLive && _isAttached) _timer.Start();
|
||||
else _timer.Stop();
|
||||
}
|
||||
|
||||
private void DrawStyleDecoration(DrawingContext ctx, Rect r)
|
||||
{
|
||||
var t = Math.Sin(_phase * Math.PI * 2d);
|
||||
switch (_styleId)
|
||||
if (_isLive && _isAttached)
|
||||
{
|
||||
case WeatherVisualStyleId.Geometric:
|
||||
DrawGeometricDecoration(ctx, r, t);
|
||||
break;
|
||||
case WeatherVisualStyleId.Breezy:
|
||||
DrawBreezyDecoration(ctx, r, t);
|
||||
break;
|
||||
case WeatherVisualStyleId.LemonFlutter:
|
||||
DrawLemonDecoration(ctx, r, t);
|
||||
break;
|
||||
_timer.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGeometricDecoration(DrawingContext ctx, Rect r, double t)
|
||||
private void RenderGoogleScene(DrawingContext ctx, Rect r, WeatherSceneProfile profile)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var t = Oscillate(0);
|
||||
|
||||
DrawRadialGlow(ctx, r.Width * 0.78 + t * 6, r.Height * 0.20 + t * 4, min * 0.55, _palette.PrimaryShape, 0.22, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.12 - t * 4, r.Height * 0.68 + t * 3, min * 0.42, _palette.SecondaryShape, 0.18, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.52, r.Height * 0.82 - t * 5, min * 0.32, _palette.AccentShape, 0.14, 0.0);
|
||||
DrawSoftBlob(ctx, r.Width * 0.78 + t * 8, r.Height * 0.18 + Oscillate(0.7) * 5, min * 0.52, _palette.PrimaryShape, 0.20);
|
||||
DrawSoftBlob(ctx, r.Width * 0.15 - t * 6, r.Height * 0.76, min * 0.36, _palette.SecondaryShape, 0.13);
|
||||
DrawSoftBlob(ctx, r.Width * 0.58, r.Height * 0.92 - t * 7, min * 0.46, _palette.AccentShape, 0.08);
|
||||
|
||||
DrawRadialGlow(ctx, r.Width * 0.35 + t * 3, r.Height * 0.12, min * 0.28, _palette.AccentShape, 0.08, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.88 - t * 2, r.Height * 0.55, min * 0.22, _palette.PrimaryShape, 0.10, 0.0);
|
||||
|
||||
DrawArcSegment(ctx, r.Width * 0.65 + t * 4, r.Height * 0.35, min * 0.38, -30, 120, _palette.SecondaryShape, 0.12, 2.5);
|
||||
DrawArcSegment(ctx, r.Width * 0.25 - t * 3, r.Height * 0.50, min * 0.30, 45, 90, _palette.AccentShape, 0.10, 2);
|
||||
}
|
||||
|
||||
private void DrawBreezyDecoration(DrawingContext ctx, Rect r, double t)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
|
||||
DrawRadialGlow(ctx, r.Width * 0.72 + t * 5, r.Height * 0.25 + t * 3, min * 0.48, _palette.PrimaryShape, 0.20, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.20 - t * 4, r.Height * 0.60 + t * 4, min * 0.36, _palette.SecondaryShape, 0.16, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.50, r.Height * 0.80 - t * 3, min * 0.28, _palette.AccentShape, 0.12, 0.0);
|
||||
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var y = r.Height * (0.25 + i * 0.18);
|
||||
var shift = Math.Sin(_phase * Math.PI * 2 + i * 1.1) * r.Width * 0.05;
|
||||
DrawWaveLine(ctx, r, y, shift, i, _palette.SurfaceTint, 0.10 + i * 0.02);
|
||||
}
|
||||
|
||||
DrawArcSegment(ctx, r.Width * 0.80 + t * 3, r.Height * 0.15, min * 0.25, 0, 180, _palette.PrimaryShape, 0.08, 1.5);
|
||||
DrawArcSegment(ctx, r.Width * 0.15 - t * 2, r.Height * 0.75, min * 0.20, 90, 180, _palette.AccentShape, 0.08, 1.5);
|
||||
}
|
||||
|
||||
private void DrawLemonDecoration(DrawingContext ctx, Rect r, double t)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
|
||||
switch (_condition)
|
||||
switch (profile.Condition)
|
||||
{
|
||||
case MaterialWeatherCondition.Clear:
|
||||
case MaterialWeatherCondition.PartlyCloudy:
|
||||
case MaterialWeatherCondition.Unknown:
|
||||
DrawSunScene(ctx, r, min, t);
|
||||
DrawSunDisk(ctx, r, 0.74, 0.24, 0.24, 0.32, rays: false);
|
||||
DrawArc(ctx, r.Width * 0.76, r.Height * 0.24, min * 0.28, 205, 110, _palette.AccentShape, 0.12, min * 0.012);
|
||||
break;
|
||||
case MaterialWeatherCondition.PartlyCloudy:
|
||||
DrawSunDisk(ctx, r, 0.76, 0.22, 0.21, 0.25, rays: false);
|
||||
DrawCloudCluster(ctx, r, 0.58 + t * 0.015, 0.38, 0.34, _palette.SurfaceTint, 0.34, filled: true);
|
||||
break;
|
||||
case MaterialWeatherCondition.Cloudy:
|
||||
DrawCloudScene(ctx, r, min, t);
|
||||
DrawCloudCluster(ctx, r, 0.48 + t * 0.012, 0.32, 0.42, _palette.SurfaceTint, 0.36, filled: true);
|
||||
DrawCloudCluster(ctx, r, 0.70 - t * 0.010, 0.52, 0.32, _palette.SecondaryShape, 0.20, filled: true);
|
||||
break;
|
||||
case MaterialWeatherCondition.Rain:
|
||||
DrawCloudCluster(ctx, r, 0.54 + t * 0.010, 0.28, 0.38, _palette.SurfaceTint, 0.30, filled: true);
|
||||
DrawRainField(ctx, r, 0.34, 0.17, _palette.AccentShape, 0.55, storm: false);
|
||||
break;
|
||||
case MaterialWeatherCondition.Storm:
|
||||
DrawRainScene(ctx, r, min, t);
|
||||
DrawCloudCluster(ctx, r, 0.50 + t * 0.010, 0.26, 0.42, _palette.SecondaryShape, 0.34, filled: true);
|
||||
DrawRainField(ctx, r, 0.36, 0.21, _palette.SurfaceTint, 0.50, storm: true);
|
||||
DrawLightning(ctx, r, 0.67, 0.44, 0.22, _palette.AccentShape, LightningOpacity());
|
||||
break;
|
||||
case MaterialWeatherCondition.Snow:
|
||||
DrawSnowScene(ctx, r, min, t);
|
||||
DrawCloudCluster(ctx, r, 0.52 + t * 0.008, 0.28, 0.36, _palette.SurfaceTint, 0.24, filled: true);
|
||||
DrawSnowField(ctx, r, _palette.AccentShape, 0.68, geometric: false);
|
||||
break;
|
||||
default:
|
||||
DrawSunScene(ctx, r, min, t);
|
||||
case MaterialWeatherCondition.Fog:
|
||||
case MaterialWeatherCondition.Haze:
|
||||
DrawFogBands(ctx, r, _palette.SurfaceTint, 0.23, curved: false);
|
||||
DrawSoftBlob(ctx, r.Width * 0.50, r.Height * 0.42, min * 0.44, _palette.SecondaryShape, 0.08);
|
||||
break;
|
||||
}
|
||||
|
||||
DrawRadialGlow(ctx, r.Width * 0.15 - t * 3, r.Height * 0.70 + t * 4, min * 0.30, _palette.SecondaryShape, 0.10, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.85 + t * 2, r.Height * 0.55 - t * 3, min * 0.22, _palette.AccentShape, 0.08, 0.0);
|
||||
}
|
||||
|
||||
private void DrawSunScene(DrawingContext ctx, Rect r, double min, double t)
|
||||
private void RenderGeometricScene(DrawingContext ctx, Rect r, WeatherSceneProfile profile)
|
||||
{
|
||||
var cx = r.Width * 0.70;
|
||||
var cy = r.Height * 0.25;
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var t = Oscillate(0.2);
|
||||
|
||||
DrawRadialGlow(ctx, cx, cy, min * 0.35, _palette.PrimaryShape, 0.28, 0.0);
|
||||
DrawRadialGlow(ctx, cx, cy, min * 0.18, _palette.PrimaryShape, 0.45, 0.10);
|
||||
DrawCircle(ctx, r.Width * 0.82 + t * 5, r.Height * 0.18, min * 0.33, _palette.PrimaryShape, 0.12);
|
||||
DrawArc(ctx, r.Width * 0.34, r.Height * 0.52 + t * 4, min * 0.42, 25, 135, _palette.SecondaryShape, 0.18, min * 0.018);
|
||||
DrawArc(ctx, r.Width * 0.72, r.Height * 0.76, min * 0.32, 198, 112, _palette.AccentShape, 0.16, min * 0.014);
|
||||
|
||||
var rayCount = 14;
|
||||
var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.18), Math.Max(2, min * 0.012), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < rayCount; i++)
|
||||
switch (profile.Condition)
|
||||
{
|
||||
var angle = (i / (double)rayCount) * Math.PI * 2 + t * 0.25;
|
||||
var innerR = min * 0.16;
|
||||
var outerR = min * 0.30 + Math.Sin(angle * 3 + t * 2) * min * 0.04;
|
||||
ctx.DrawLine(pen,
|
||||
new Point(cx + Math.Cos(angle) * innerR, cy + Math.Sin(angle) * innerR),
|
||||
new Point(cx + Math.Cos(angle) * outerR, cy + Math.Sin(angle) * outerR));
|
||||
case MaterialWeatherCondition.Clear:
|
||||
case MaterialWeatherCondition.Unknown:
|
||||
DrawCircle(ctx, r.Width * 0.72, r.Height * 0.28, min * 0.21, _palette.PrimaryShape, 0.34);
|
||||
DrawSunRays(ctx, r.Width * 0.72, r.Height * 0.28, min * 0.24, min * 0.38, 12, _palette.PrimaryShape, 0.18);
|
||||
DrawArc(ctx, r.Width * 0.72, r.Height * 0.28, min * 0.30, -20, 230, _palette.AccentShape, 0.22, min * 0.016);
|
||||
break;
|
||||
case MaterialWeatherCondition.PartlyCloudy:
|
||||
DrawCircle(ctx, r.Width * 0.72, r.Height * 0.24, min * 0.18, _palette.PrimaryShape, 0.25);
|
||||
DrawCloudCluster(ctx, r, 0.56 + t * 0.012, 0.40, 0.34, _palette.SecondaryShape, 0.28, filled: false);
|
||||
DrawCircle(ctx, r.Width * 0.49, r.Height * 0.42, min * 0.18, _palette.SurfaceTint, 0.12);
|
||||
break;
|
||||
case MaterialWeatherCondition.Cloudy:
|
||||
DrawCloudCluster(ctx, r, 0.44 + t * 0.010, 0.34, 0.40, _palette.SecondaryShape, 0.27, filled: false);
|
||||
DrawCloudCluster(ctx, r, 0.68 - t * 0.010, 0.52, 0.31, _palette.AccentShape, 0.16, filled: false);
|
||||
DrawArc(ctx, r.Width * 0.58, r.Height * 0.44, min * 0.36, 190, 135, _palette.SurfaceTint, 0.19, min * 0.012);
|
||||
break;
|
||||
case MaterialWeatherCondition.Rain:
|
||||
DrawCloudCluster(ctx, r, 0.50, 0.28, 0.38, _palette.SecondaryShape, 0.24, filled: false);
|
||||
DrawGeometricRainGrid(ctx, r, _palette.AccentShape, 0.60, storm: false);
|
||||
break;
|
||||
case MaterialWeatherCondition.Storm:
|
||||
DrawCloudCluster(ctx, r, 0.48, 0.26, 0.42, _palette.SecondaryShape, 0.24, filled: false);
|
||||
DrawGeometricRainGrid(ctx, r, _palette.SurfaceTint, 0.52, storm: true);
|
||||
DrawLightning(ctx, r, 0.65, 0.43, 0.26, _palette.AccentShape, LightningOpacity());
|
||||
DrawTriangle(ctx, r.Width * 0.33, r.Height * 0.68, min * 0.18, _palette.PrimaryShape, 0.12, rotate: 0.35);
|
||||
break;
|
||||
case MaterialWeatherCondition.Snow:
|
||||
DrawCloudCluster(ctx, r, 0.50, 0.28, 0.36, _palette.SecondaryShape, 0.18, filled: false);
|
||||
DrawSnowField(ctx, r, _palette.AccentShape, 0.72, geometric: true);
|
||||
break;
|
||||
case MaterialWeatherCondition.Fog:
|
||||
case MaterialWeatherCondition.Haze:
|
||||
DrawFogBands(ctx, r, _palette.SurfaceTint, 0.25, curved: false);
|
||||
DrawArc(ctx, r.Width * 0.44, r.Height * 0.50, min * 0.36, 0, 180, _palette.SecondaryShape, 0.16, min * 0.016);
|
||||
DrawArc(ctx, r.Width * 0.64, r.Height * 0.62, min * 0.30, 180, 170, _palette.AccentShape, 0.12, min * 0.012);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCloudScene(DrawingContext ctx, Rect r, double min, double t)
|
||||
private void RenderBreezyScene(DrawingContext ctx, Rect r, WeatherSceneProfile profile)
|
||||
{
|
||||
DrawRadialGlow(ctx, r.Width * 0.60 + t * 5, r.Height * 0.30, min * 0.40, _palette.PrimaryShape, 0.16, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.35 - t * 3, r.Height * 0.55, min * 0.32, _palette.SecondaryShape, 0.12, 0.0);
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var t = Oscillate(0.4);
|
||||
|
||||
var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.14), Math.Max(1.5, min * 0.010), lineCap: PenLineCap.Round);
|
||||
var drift = t * 6;
|
||||
DrawSoftBlob(ctx, r.Width * 0.76 + t * 7, r.Height * 0.18, min * 0.48, _palette.PrimaryShape, 0.18);
|
||||
DrawSoftBlob(ctx, r.Width * 0.18 - t * 5, r.Height * 0.62, min * 0.42, _palette.SecondaryShape, 0.12);
|
||||
DrawWaveField(ctx, r, _palette.SurfaceTint, 0.11, 4, amplitudeScale: 1.0);
|
||||
|
||||
DrawCloudOutline(ctx, r.Width * 0.42 + drift, r.Height * 0.32, min * 0.18, min * 0.12, pen);
|
||||
DrawCloudOutline(ctx, r.Width * 0.58 + drift * 0.7, r.Height * 0.26, min * 0.22, min * 0.15, pen);
|
||||
DrawCloudOutline(ctx, r.Width * 0.72 + drift * 0.5, r.Height * 0.35, min * 0.14, min * 0.10, pen);
|
||||
}
|
||||
|
||||
private void DrawRainScene(DrawingContext ctx, Rect r, double min, double t)
|
||||
{
|
||||
DrawRadialGlow(ctx, r.Width * 0.65 + t * 4, r.Height * 0.25, min * 0.38, _palette.PrimaryShape, 0.14, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.30 - t * 3, r.Height * 0.50, min * 0.30, _palette.SecondaryShape, 0.10, 0.0);
|
||||
|
||||
var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.10), Math.Max(1, r.Width / 200), lineCap: PenLineCap.Round);
|
||||
var streaks = Math.Clamp((int)(r.Width / 28), 6, 16);
|
||||
for (var i = 0; i < streaks; i++)
|
||||
switch (profile.Condition)
|
||||
{
|
||||
var progress = (_phase * 0.5 + i * 0.12) % 1d;
|
||||
var x = r.Width * (0.12 + (i % streaks) / (double)streaks * 0.78);
|
||||
var y = r.Height * (0.15 + progress * 0.75);
|
||||
var len = r.Height * 0.08;
|
||||
ctx.DrawLine(pen, new Point(x, y), new Point(x - r.Width * 0.018, y + len));
|
||||
case MaterialWeatherCondition.Clear:
|
||||
case MaterialWeatherCondition.Unknown:
|
||||
DrawSunDisk(ctx, r, 0.72, 0.28, 0.23, 0.24, rays: false);
|
||||
DrawWaveField(ctx, r, _palette.AccentShape, 0.12, 3, amplitudeScale: 0.75);
|
||||
DrawArc(ctx, r.Width * 0.76, r.Height * 0.28, min * 0.30, 205, 145, _palette.PrimaryShape, 0.16, min * 0.012);
|
||||
break;
|
||||
case MaterialWeatherCondition.PartlyCloudy:
|
||||
DrawSunDisk(ctx, r, 0.73, 0.24, 0.18, 0.18, rays: false);
|
||||
DrawBreezyCloudBands(ctx, r, yBase: 0.42, density: 3, alpha: 0.24);
|
||||
DrawWaveField(ctx, r, _palette.AccentShape, 0.10, 3, amplitudeScale: 0.65);
|
||||
break;
|
||||
case MaterialWeatherCondition.Cloudy:
|
||||
DrawBreezyCloudBands(ctx, r, yBase: 0.30, density: 5, alpha: 0.26);
|
||||
DrawSoftBlob(ctx, r.Width * 0.58, r.Height * 0.44, min * 0.35, _palette.SurfaceTint, 0.14);
|
||||
break;
|
||||
case MaterialWeatherCondition.Rain:
|
||||
DrawBreezyCloudBands(ctx, r, yBase: 0.26, density: 4, alpha: 0.26);
|
||||
DrawRainBands(ctx, r, _palette.AccentShape, 0.48, storm: false);
|
||||
DrawWaveField(ctx, r, _palette.SecondaryShape, 0.14, 4, amplitudeScale: 1.25);
|
||||
break;
|
||||
case MaterialWeatherCondition.Storm:
|
||||
DrawBreezyCloudBands(ctx, r, yBase: 0.24, density: 5, alpha: 0.30);
|
||||
DrawRainBands(ctx, r, _palette.SurfaceTint, 0.48, storm: true);
|
||||
DrawLightning(ctx, r, 0.64, 0.42, 0.23, _palette.AccentShape, LightningOpacity());
|
||||
DrawWaveField(ctx, r, _palette.AccentShape, 0.16, 5, amplitudeScale: 1.35);
|
||||
break;
|
||||
case MaterialWeatherCondition.Snow:
|
||||
DrawBreezyCloudBands(ctx, r, yBase: 0.28, density: 3, alpha: 0.20);
|
||||
DrawSnowField(ctx, r, _palette.AccentShape, 0.68, geometric: true);
|
||||
DrawWaveField(ctx, r, Colors.White, 0.13, 3, amplitudeScale: 0.85);
|
||||
break;
|
||||
case MaterialWeatherCondition.Fog:
|
||||
case MaterialWeatherCondition.Haze:
|
||||
DrawFogBands(ctx, r, _palette.SurfaceTint, 0.28, curved: true);
|
||||
DrawWaveField(ctx, r, _palette.SecondaryShape, 0.18, 5, amplitudeScale: 0.55);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSnowScene(DrawingContext ctx, Rect r, double min, double t)
|
||||
private void RenderLemonScene(DrawingContext ctx, Rect r, WeatherSceneProfile profile)
|
||||
{
|
||||
DrawRadialGlow(ctx, r.Width * 0.68 + t * 3, r.Height * 0.22, min * 0.35, _palette.PrimaryShape, 0.16, 0.0);
|
||||
DrawRadialGlow(ctx, r.Width * 0.25 - t * 2, r.Height * 0.55, min * 0.28, _palette.AccentShape, 0.10, 0.0);
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var t = Oscillate(0.6);
|
||||
|
||||
var cx = r.Width * 0.72;
|
||||
var cy = r.Height * 0.28;
|
||||
var sr = min * 0.12;
|
||||
var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.16), Math.Max(1.2, min * 0.008), lineCap: PenLineCap.Round);
|
||||
DrawSoftBlob(ctx, r.Width * 0.78 + t * 6, r.Height * 0.20, min * 0.45, _palette.PrimaryShape, 0.18);
|
||||
DrawCircle(ctx, r.Width * 0.18, r.Height * 0.78 - t * 5, min * 0.20, _palette.SecondaryShape, 0.13);
|
||||
DrawCircle(ctx, r.Width * 0.88, r.Height * 0.64, min * 0.16, _palette.AccentShape, 0.10);
|
||||
|
||||
switch (profile.Condition)
|
||||
{
|
||||
case MaterialWeatherCondition.Clear:
|
||||
case MaterialWeatherCondition.Unknown:
|
||||
DrawSunDisk(ctx, r, 0.70, 0.30, 0.23, 0.30, rays: true);
|
||||
DrawCircle(ctx, r.Width * 0.36, r.Height * 0.30, min * 0.07, _palette.SecondaryShape, 0.16);
|
||||
break;
|
||||
case MaterialWeatherCondition.PartlyCloudy:
|
||||
DrawSunDisk(ctx, r, 0.73, 0.24, 0.20, 0.24, rays: true);
|
||||
DrawCloudCluster(ctx, r, 0.56 + t * 0.012, 0.40, 0.34, _palette.SurfaceTint, 0.30, filled: true);
|
||||
break;
|
||||
case MaterialWeatherCondition.Cloudy:
|
||||
DrawCloudCluster(ctx, r, 0.48 + t * 0.012, 0.34, 0.42, _palette.SurfaceTint, 0.31, filled: true);
|
||||
DrawCloudCluster(ctx, r, 0.70 - t * 0.010, 0.53, 0.28, _palette.SecondaryShape, 0.18, filled: true);
|
||||
DrawCircle(ctx, r.Width * 0.28, r.Height * 0.44, min * 0.08, _palette.AccentShape, 0.12);
|
||||
break;
|
||||
case MaterialWeatherCondition.Rain:
|
||||
DrawCloudCluster(ctx, r, 0.52, 0.28, 0.40, _palette.SurfaceTint, 0.28, filled: true);
|
||||
DrawRainField(ctx, r, 0.36, 0.18, _palette.AccentShape, 0.55, storm: false);
|
||||
DrawCircle(ctx, r.Width * 0.23, r.Height * 0.72, min * 0.09, _palette.PrimaryShape, 0.12);
|
||||
break;
|
||||
case MaterialWeatherCondition.Storm:
|
||||
DrawCloudCluster(ctx, r, 0.50, 0.26, 0.42, _palette.SurfaceTint, 0.30, filled: true);
|
||||
DrawRainField(ctx, r, 0.36, 0.22, _palette.SecondaryShape, 0.52, storm: true);
|
||||
DrawLightning(ctx, r, 0.66, 0.42, 0.24, _palette.AccentShape, LightningOpacity());
|
||||
break;
|
||||
case MaterialWeatherCondition.Snow:
|
||||
DrawCloudCluster(ctx, r, 0.52, 0.30, 0.38, _palette.SurfaceTint, 0.22, filled: true);
|
||||
DrawSnowField(ctx, r, _palette.AccentShape, 0.72, geometric: true);
|
||||
break;
|
||||
case MaterialWeatherCondition.Fog:
|
||||
case MaterialWeatherCondition.Haze:
|
||||
DrawFogBands(ctx, r, _palette.SurfaceTint, 0.26, curved: true);
|
||||
DrawCircle(ctx, r.Width * 0.70, r.Height * 0.28, min * 0.16, _palette.SecondaryShape, 0.10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSunDisk(DrawingContext ctx, Rect r, double nx, double ny, double radiusScale, double alpha, bool rays)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var cx = r.Width * nx + Oscillate(0.1) * min * 0.015;
|
||||
var cy = r.Height * ny + Oscillate(0.9) * min * 0.012;
|
||||
var radius = min * radiusScale;
|
||||
|
||||
DrawSoftBlob(ctx, cx, cy, radius * 1.85, _palette.PrimaryShape, alpha * 0.55);
|
||||
DrawCircle(ctx, cx, cy, radius, _palette.PrimaryShape, alpha);
|
||||
DrawCircle(ctx, cx - radius * 0.25, cy - radius * 0.28, radius * 0.36, _palette.AccentShape, alpha * 0.32);
|
||||
if (rays)
|
||||
{
|
||||
DrawSunRays(ctx, cx, cy, radius * 1.05, radius * 1.78, 14, _palette.PrimaryShape, alpha * 0.38);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCloudCluster(DrawingContext ctx, Rect r, double nx, double ny, double scale, Color color, double alpha, bool filled)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var cx = r.Width * nx;
|
||||
var cy = r.Height * ny;
|
||||
var brush = filled ? new SolidColorBrush(color, alpha) : null;
|
||||
var pen = filled ? null : new Pen(new SolidColorBrush(color, alpha), Math.Max(1.4, min * 0.012), lineCap: PenLineCap.Round);
|
||||
var radius = min * scale;
|
||||
|
||||
DrawEllipse(ctx, brush, pen, cx - radius * 0.34, cy + radius * 0.04, radius * 0.34, radius * 0.18);
|
||||
DrawEllipse(ctx, brush, pen, cx, cy - radius * 0.06, radius * 0.42, radius * 0.24);
|
||||
DrawEllipse(ctx, brush, pen, cx + radius * 0.34, cy + radius * 0.08, radius * 0.30, radius * 0.17);
|
||||
|
||||
if (filled)
|
||||
{
|
||||
var baseRect = new Rect(cx - radius * 0.66, cy + radius * 0.04, radius * 1.24, radius * 0.25);
|
||||
ctx.DrawRectangle(new SolidColorBrush(color, alpha * 0.78), null, baseRect, radius * 0.12, radius * 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBreezyCloudBands(DrawingContext ctx, Rect r, double yBase, int density, double alpha)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
for (var i = 0; i < density; i++)
|
||||
{
|
||||
var y = r.Height * (yBase + i * 0.085);
|
||||
var shift = Oscillate(i * 0.32) * r.Width * 0.035;
|
||||
var thickness = Math.Max(8, min * (0.075 - i * 0.006));
|
||||
var brush = new SolidColorBrush(i % 2 == 0 ? _palette.SurfaceTint : _palette.SecondaryShape, alpha * (1 - i * 0.10));
|
||||
ctx.DrawRectangle(
|
||||
brush,
|
||||
null,
|
||||
new Rect(r.Width * (0.06 + i * 0.025) + shift, y, r.Width * (0.84 - i * 0.055), thickness),
|
||||
thickness * 0.5,
|
||||
thickness * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRainField(DrawingContext ctx, Rect r, double startY, double densityScale, Color color, double alpha, bool storm)
|
||||
{
|
||||
var count = Math.Clamp((int)(r.Width * densityScale), 8, storm ? 32 : 24);
|
||||
var pen = new Pen(new SolidColorBrush(color, alpha), Math.Max(1.2, r.Width / 160), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var p = (_phase * (storm ? 1.4 : 0.95) + i * 0.137) % 1d;
|
||||
var lane = (i + 0.37 * (i % 3)) / count;
|
||||
var x = r.Width * (0.08 + lane * 0.84);
|
||||
var y = r.Height * (startY + p * 0.74);
|
||||
var dx = -r.Width * (storm ? 0.040 : 0.026);
|
||||
var dy = r.Height * (storm ? 0.13 : 0.095);
|
||||
ctx.DrawLine(pen, new Point(x, y), new Point(x + dx, y + dy));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGeometricRainGrid(DrawingContext ctx, Rect r, Color color, double alpha, bool storm)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var count = Math.Clamp((int)(r.Width / 18), 9, storm ? 28 : 22);
|
||||
var pen = new Pen(new SolidColorBrush(color, alpha), Math.Max(1.3, min * 0.009), lineCap: PenLineCap.Square);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var p = (_phase * (storm ? 1.15 : 0.75) + i * 0.091) % 1d;
|
||||
var x = r.Width * (0.12 + (i / (double)count) * 0.78);
|
||||
var y = r.Height * (0.36 + p * 0.58);
|
||||
ctx.DrawLine(pen, new Point(x, y), new Point(x - min * 0.075, y + min * 0.145));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRainBands(DrawingContext ctx, Rect r, Color color, double alpha, bool storm)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var count = Math.Clamp((int)(r.Width / 22), 8, storm ? 26 : 20);
|
||||
var pen = new Pen(new SolidColorBrush(color, alpha), Math.Max(2.2, min * 0.014), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var p = (_phase * (storm ? 1.35 : 0.85) + i * 0.118) % 1d;
|
||||
var x = r.Width * (0.10 + (i / (double)count) * 0.86);
|
||||
var y = r.Height * (0.34 + p * 0.62);
|
||||
ctx.DrawLine(pen, new Point(x, y), new Point(x - min * 0.09, y + min * 0.16));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSnowField(DrawingContext ctx, Rect r, Color color, double alpha, bool geometric)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var count = Math.Clamp((int)(r.Width / 22), 8, 24);
|
||||
var brush = new SolidColorBrush(color, alpha);
|
||||
var pen = new Pen(brush, Math.Max(1.1, min * 0.007), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var p = (_phase * 0.45 + i * 0.119) % 1d;
|
||||
var x = r.Width * (0.10 + (i / (double)count) * 0.82) + Math.Sin(p * Math.PI * 2 + i) * min * 0.025;
|
||||
var y = r.Height * (0.22 + p * 0.78);
|
||||
if (geometric && i % 3 == 0)
|
||||
{
|
||||
DrawSnowflake(ctx, x, y, min * 0.025, pen);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.DrawEllipse(brush, null, new Point(x, y), Math.Max(1.8, min * 0.012), Math.Max(1.8, min * 0.012));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFogBands(DrawingContext ctx, Rect r, Color color, double alpha, bool curved)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var count = 5;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var y = r.Height * (0.35 + i * 0.105);
|
||||
var shift = Oscillate(i * 0.25) * r.Width * 0.045;
|
||||
var pen = new Pen(new SolidColorBrush(color, alpha * (1 - i * 0.08)), Math.Max(2.2, min * 0.015), lineCap: PenLineCap.Round);
|
||||
if (curved)
|
||||
{
|
||||
DrawWavePath(ctx, r.Width * 0.10 + shift, y, r.Width * 0.82, min * 0.020, i, pen);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.DrawLine(pen, new Point(r.Width * 0.12 + shift, y), new Point(r.Width * 0.88 + shift, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWaveField(DrawingContext ctx, Rect r, Color color, double alpha, int lines, double amplitudeScale)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
for (var i = 0; i < lines; i++)
|
||||
{
|
||||
var y = r.Height * (0.22 + i * 0.16);
|
||||
var shift = Oscillate(i * 0.22) * r.Width * 0.06;
|
||||
var pen = new Pen(new SolidColorBrush(color, alpha * (1 - i * 0.06)), Math.Max(1.6, min * 0.010), lineCap: PenLineCap.Round);
|
||||
DrawWavePath(ctx, r.Width * 0.06 + shift, y, r.Width * 0.88, min * 0.030 * amplitudeScale, i, pen);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWavePath(DrawingContext ctx, double startX, double baseY, double width, double amplitude, int index, Pen pen)
|
||||
{
|
||||
var stream = new StreamGeometry();
|
||||
using (var g = stream.Open())
|
||||
{
|
||||
g.BeginFigure(new Point(startX, baseY), false);
|
||||
var step = Math.Max(3, width / 48);
|
||||
for (var x = 0d; x <= width; x += step)
|
||||
{
|
||||
var y = baseY + Math.Sin((x / width) * Math.PI * 3.2 + _phase * Math.PI * 2 + index * 0.85) * amplitude;
|
||||
g.LineTo(new Point(startX + x, y));
|
||||
}
|
||||
g.EndFigure(false);
|
||||
}
|
||||
|
||||
ctx.DrawGeometry(null, pen, stream);
|
||||
}
|
||||
|
||||
private void DrawLightning(DrawingContext ctx, Rect r, double nx, double ny, double scale, Color color, double alpha)
|
||||
{
|
||||
var min = Math.Min(r.Width, r.Height);
|
||||
var cx = r.Width * nx;
|
||||
var cy = r.Height * ny;
|
||||
var s = min * scale;
|
||||
var bolt = new StreamGeometry();
|
||||
using (var g = bolt.Open())
|
||||
{
|
||||
g.BeginFigure(new Point(cx, cy), true);
|
||||
g.LineTo(new Point(cx - s * 0.28, cy + s * 0.46));
|
||||
g.LineTo(new Point(cx - s * 0.03, cy + s * 0.40));
|
||||
g.LineTo(new Point(cx - s * 0.36, cy + s * 0.98));
|
||||
g.LineTo(new Point(cx + s * 0.18, cy + s * 0.25));
|
||||
g.LineTo(new Point(cx - s * 0.05, cy + s * 0.31));
|
||||
g.EndFigure(true);
|
||||
}
|
||||
|
||||
ctx.DrawGeometry(new SolidColorBrush(color, alpha), null, bolt);
|
||||
}
|
||||
|
||||
private void DrawSunRays(DrawingContext ctx, double cx, double cy, double inner, double outer, int count, Color color, double alpha)
|
||||
{
|
||||
var pen = new Pen(new SolidColorBrush(color, alpha), Math.Max(1.4, inner * 0.055), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var angle = (i / (double)count) * Math.PI * 2 + _phase * 0.45;
|
||||
var outRadius = outer + Math.Sin(angle * 2.4 + _phase * Math.PI * 2) * inner * 0.16;
|
||||
ctx.DrawLine(
|
||||
pen,
|
||||
new Point(cx + Math.Cos(angle) * inner, cy + Math.Sin(angle) * inner),
|
||||
new Point(cx + Math.Cos(angle) * outRadius, cy + Math.Sin(angle) * outRadius));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSnowflake(DrawingContext ctx, double cx, double cy, double radius, Pen pen)
|
||||
{
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var a = (i / 6d) * Math.PI * 2 + t * 0.15;
|
||||
var ex = cx + Math.Cos(a) * sr;
|
||||
var ey = cy + Math.Sin(a) * sr;
|
||||
ctx.DrawLine(pen, new Point(cx, cy), new Point(ex, ey));
|
||||
var br = sr * 0.35;
|
||||
var mx = cx + Math.Cos(a) * sr * 0.6;
|
||||
var my = cy + Math.Sin(a) * sr * 0.6;
|
||||
ctx.DrawLine(pen, new Point(mx, my), new Point(mx + Math.Cos(a + 0.5) * br, my + Math.Sin(a + 0.5) * br));
|
||||
ctx.DrawLine(pen, new Point(mx, my), new Point(mx + Math.Cos(a - 0.5) * br, my + Math.Sin(a - 0.5) * br));
|
||||
var a = (i / 6d) * Math.PI * 2 + _phase * 0.35;
|
||||
ctx.DrawLine(pen, new Point(cx - Math.Cos(a) * radius * 0.45, cy - Math.Sin(a) * radius * 0.45), new Point(cx + Math.Cos(a) * radius, cy + Math.Sin(a) * radius));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRadialGlow(DrawingContext ctx, double cx, double cy, double radius, Color baseColor, double peakAlpha, double centerBoost)
|
||||
private void DrawTriangle(DrawingContext ctx, double cx, double cy, double radius, Color color, double alpha, double rotate)
|
||||
{
|
||||
if (radius < 1) return;
|
||||
var triangle = new StreamGeometry();
|
||||
using (var g = triangle.Open())
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var a = rotate + (i / 3d) * Math.PI * 2;
|
||||
var p = new Point(cx + Math.Cos(a) * radius, cy + Math.Sin(a) * radius);
|
||||
if (i == 0)
|
||||
{
|
||||
g.BeginFigure(p, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.LineTo(p);
|
||||
}
|
||||
}
|
||||
|
||||
var peak = (byte)Math.Clamp(peakAlpha * 255, 0, 255);
|
||||
var edge = (byte)0;
|
||||
var center = (byte)Math.Clamp(centerBoost * 255, 0, 255);
|
||||
g.EndFigure(true);
|
||||
}
|
||||
|
||||
ctx.DrawGeometry(new SolidColorBrush(color, alpha), null, triangle);
|
||||
}
|
||||
|
||||
private void DrawCircle(DrawingContext ctx, double cx, double cy, double radius, Color color, double alpha)
|
||||
{
|
||||
if (radius <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.DrawEllipse(new SolidColorBrush(color, alpha), null, new Point(cx, cy), radius, radius);
|
||||
}
|
||||
|
||||
private void DrawSoftBlob(DrawingContext ctx, double cx, double cy, double radius, Color color, double peakAlpha)
|
||||
{
|
||||
if (radius <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var brush = new RadialGradientBrush
|
||||
{
|
||||
Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(new Color(Math.Clamp((byte)(peak + center), (byte)0, (byte)255), baseColor.R, baseColor.G, baseColor.B), 0),
|
||||
new GradientStop(new Color((byte)(peak * 0.6), baseColor.R, baseColor.G, baseColor.B), 0.4),
|
||||
new GradientStop(new Color(edge, baseColor.R, baseColor.G, baseColor.B), 1)
|
||||
new GradientStop(WithAlpha(color, peakAlpha), 0),
|
||||
new GradientStop(WithAlpha(color, peakAlpha * 0.52), 0.42),
|
||||
new GradientStop(WithAlpha(color, 0), 1)
|
||||
}
|
||||
};
|
||||
|
||||
ctx.DrawEllipse(brush, null, new Point(cx, cy), radius, radius);
|
||||
}
|
||||
|
||||
private void DrawArcSegment(DrawingContext ctx, double cx, double cy, double radius, double startDeg, double sweepDeg, Color color, double alpha, double thickness)
|
||||
private static void DrawEllipse(DrawingContext ctx, IBrush? brush, Pen? pen, double cx, double cy, double rx, double ry)
|
||||
{
|
||||
if (radius < 2) return;
|
||||
ctx.DrawEllipse(brush, pen, new Point(cx, cy), Math.Max(0.1, rx), Math.Max(0.1, ry));
|
||||
}
|
||||
|
||||
var pen = new Pen(new SolidColorBrush(color, (float)alpha), thickness, lineCap: PenLineCap.Round);
|
||||
private void DrawArc(DrawingContext ctx, double cx, double cy, double radius, double startDeg, double sweepDeg, Color color, double alpha, double thickness)
|
||||
{
|
||||
if (radius < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stream = new StreamGeometry();
|
||||
var g = stream.Open();
|
||||
|
||||
var startRad = startDeg * Math.PI / 180d;
|
||||
var sweepRad = sweepDeg * Math.PI / 180d;
|
||||
var steps = Math.Max(8, (int)(sweepDeg / 5));
|
||||
|
||||
g.BeginFigure(new Point(cx + Math.Cos(startRad) * radius, cy + Math.Sin(startRad) * radius), false);
|
||||
for (var i = 1; i <= steps; i++)
|
||||
using (var g = stream.Open())
|
||||
{
|
||||
var a = startRad + sweepRad * (i / (double)steps);
|
||||
g.LineTo(new Point(cx + Math.Cos(a) * radius, cy + Math.Sin(a) * radius));
|
||||
}
|
||||
g.EndFigure(false);
|
||||
var startRad = startDeg * Math.PI / 180d;
|
||||
var sweepRad = sweepDeg * Math.PI / 180d;
|
||||
var steps = Math.Max(10, (int)(Math.Abs(sweepDeg) / 4));
|
||||
g.BeginFigure(new Point(cx + Math.Cos(startRad) * radius, cy + Math.Sin(startRad) * radius), false);
|
||||
for (var i = 1; i <= steps; i++)
|
||||
{
|
||||
var a = startRad + sweepRad * (i / (double)steps);
|
||||
g.LineTo(new Point(cx + Math.Cos(a) * radius, cy + Math.Sin(a) * radius));
|
||||
}
|
||||
|
||||
ctx.DrawGeometry(null, pen, stream);
|
||||
g.EndFigure(false);
|
||||
}
|
||||
|
||||
ctx.DrawGeometry(null, new Pen(new SolidColorBrush(color, alpha), Math.Max(1, thickness), lineCap: PenLineCap.Round), stream);
|
||||
}
|
||||
|
||||
private void DrawWaveLine(DrawingContext ctx, Rect r, double baseY, double shift, int index, Color color, double alpha)
|
||||
private double Oscillate(double offset)
|
||||
{
|
||||
var pen = new Pen(new SolidColorBrush(color, (float)alpha), Math.Max(1.5, r.Width / 100), lineCap: PenLineCap.Round);
|
||||
var startX = r.Width * 0.05 + shift;
|
||||
var endX = r.Width * 0.95 + shift;
|
||||
|
||||
var stream = new StreamGeometry();
|
||||
var g = stream.Open();
|
||||
g.BeginFigure(new Point(startX, baseY), false);
|
||||
for (var x = startX; x <= endX; x += 3)
|
||||
{
|
||||
var waveY = baseY + Math.Sin((x - startX) / (endX - startX) * Math.PI * 3 + _phase * Math.PI * 2 + index * 1.3) * (5 + index * 2.5);
|
||||
g.LineTo(new Point(x, waveY));
|
||||
}
|
||||
g.EndFigure(false);
|
||||
ctx.DrawGeometry(null, pen, stream);
|
||||
return Math.Sin((_phase + offset) * Math.PI * 2d);
|
||||
}
|
||||
|
||||
private void DrawCloudOutline(DrawingContext ctx, double cx, double cy, double rx, double ry, Pen pen)
|
||||
private double LightningOpacity()
|
||||
{
|
||||
ctx.DrawEllipse(null, pen, new Point(cx, cy), rx, ry);
|
||||
ctx.DrawEllipse(null, pen, new Point(cx + rx * 0.6, cy - ry * 0.3), rx * 0.7, ry * 0.7);
|
||||
ctx.DrawEllipse(null, pen, new Point(cx - rx * 0.4, cy + ry * 0.2), rx * 0.5, ry * 0.5);
|
||||
if (!_isLive)
|
||||
{
|
||||
return 0.58;
|
||||
}
|
||||
|
||||
var pulse = Math.Pow(Math.Max(0, Math.Sin((_phase * 2.8 + 0.15) * Math.PI * 2)), 7);
|
||||
return 0.42 + pulse * 0.46;
|
||||
}
|
||||
|
||||
private void DrawRain(DrawingContext ctx, Rect rect, bool storm)
|
||||
private static bool EstimateNightFromPalette(MaterialWeatherPalette palette)
|
||||
{
|
||||
var drops = Math.Clamp((int)(rect.Width / 22), 8, 22);
|
||||
var brush = new SolidColorBrush(_palette.AccentShape, storm ? 0.72 : 0.52);
|
||||
var pen = new Pen(brush, Math.Max(1.4, rect.Width / 150), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < drops; i++)
|
||||
{
|
||||
var t = (_phase + i * 0.137) % 1d;
|
||||
var x = rect.Width * (0.18 + (i % drops) / (double)drops * 0.72);
|
||||
var y = rect.Height * (0.36 + t * 0.66);
|
||||
ctx.DrawLine(pen, new Point(x, y), new Point(x - rect.Width * 0.025, y + rect.Height * 0.09));
|
||||
}
|
||||
|
||||
if (storm)
|
||||
{
|
||||
var bolt = new StreamGeometry();
|
||||
var g = bolt.Open();
|
||||
g.BeginFigure(new Point(rect.Width * 0.70, rect.Height * 0.42), true);
|
||||
g.LineTo(new Point(rect.Width * 0.61, rect.Height * 0.64));
|
||||
g.LineTo(new Point(rect.Width * 0.69, rect.Height * 0.61));
|
||||
g.LineTo(new Point(rect.Width * 0.58, rect.Height * 0.86));
|
||||
g.EndFigure(true);
|
||||
ctx.DrawGeometry(new SolidColorBrush(_palette.AccentShape, 0.86), null, bolt);
|
||||
}
|
||||
static double Luma(Color color) => (0.2126 * color.R + 0.7152 * color.G + 0.0722 * color.B) / 255d;
|
||||
return (Luma(palette.BackgroundTop) + Luma(palette.BackgroundBottom)) * 0.5 < 0.36;
|
||||
}
|
||||
|
||||
private void DrawSnow(DrawingContext ctx, Rect rect)
|
||||
private static Color WithAlpha(Color color, double alpha)
|
||||
{
|
||||
var flakes = Math.Clamp((int)(rect.Width / 24), 7, 20);
|
||||
var brush = new SolidColorBrush(Colors.White, 0.72);
|
||||
for (var i = 0; i < flakes; i++)
|
||||
{
|
||||
var t = (_phase * 0.45 + i * 0.113) % 1d;
|
||||
var x = rect.Width * (0.12 + (i % flakes) / (double)flakes * 0.78) + Math.Sin(t * Math.PI * 2) * 8;
|
||||
var y = rect.Height * (0.20 + t * 0.82);
|
||||
ctx.DrawEllipse(brush, null, new Point(x, y), 2.2, 2.2);
|
||||
}
|
||||
return new Color((byte)Math.Clamp(alpha * 255, 0, 255), color.R, color.G, color.B);
|
||||
}
|
||||
|
||||
private void DrawFog(DrawingContext ctx, Rect rect)
|
||||
{
|
||||
var pen = new Pen(new SolidColorBrush(_palette.TextSecondary, 0.28), Math.Max(2, rect.Height / 56), lineCap: PenLineCap.Round);
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var y = rect.Height * (0.48 + i * 0.11);
|
||||
var shift = Math.Sin(_phase * Math.PI * 2 + i) * rect.Width * 0.04;
|
||||
ctx.DrawLine(pen, new Point(rect.Width * 0.18 + shift, y), new Point(rect.Width * 0.82 + shift, y));
|
||||
}
|
||||
}
|
||||
|
||||
private IBrush CreateLinearBrush(Color top, Color bottom, double sx, double sy, double ex, double ey)
|
||||
private static IBrush CreateLinearBrush(Color top, Color bottom, double sx, double sy, double ex, double ey)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
||||
{
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
{
|
||||
@@ -28,6 +28,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
private double _currentCellSize = 48;
|
||||
private bool _isAttached;
|
||||
private bool _isOnActivePage = true;
|
||||
private bool _isDisposed;
|
||||
|
||||
public MusicControlWidget()
|
||||
{
|
||||
@@ -44,6 +45,19 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
ApplyViewModel();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
_refreshTimer.Stop();
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
@@ -71,6 +71,8 @@ public abstract class WeatherWidgetBase : UserControl,
|
||||
|
||||
protected string CurrentVisualStyleId { get; private set; } = WeatherVisualStyleId.Default;
|
||||
|
||||
protected bool CurrentIsNight { get; private set; }
|
||||
|
||||
protected bool IsLiveRenderMode => _renderMode == DesktopComponentRenderMode.Live;
|
||||
|
||||
protected double CurrentCellSize => _cellSize;
|
||||
@@ -200,7 +202,7 @@ public abstract class WeatherWidgetBase : UserControl,
|
||||
|
||||
protected void ApplyCurrentScene()
|
||||
{
|
||||
SceneControl.Apply(CurrentVisualStyleId, CurrentCondition, CurrentPalette, IsLiveRenderMode && _isAttached && _isOnActivePage && !_isEditMode);
|
||||
SceneControl.Apply(CurrentVisualStyleId, CurrentCondition, CurrentPalette, IsLiveRenderMode && _isAttached && _isOnActivePage && !_isEditMode, CurrentIsNight);
|
||||
}
|
||||
|
||||
protected string ResolveIconKey(int? weatherCode, string? weatherText, bool isDaylight = true)
|
||||
@@ -320,6 +322,7 @@ public abstract class WeatherWidgetBase : UserControl,
|
||||
: _settingsFacade.Theme.Get().IsNightMode;
|
||||
CurrentVisualStyleId = WeatherVisualStyleCatalog.Normalize(_settingsFacade.Weather.Get().IconPackId);
|
||||
CurrentCondition = MaterialWeatherVisualTheme.ResolveCondition(snapshot);
|
||||
CurrentIsNight = isNight;
|
||||
CurrentPalette = MaterialWeatherVisualTheme.ResolvePalette(CurrentVisualStyleId, CurrentCondition, isNight);
|
||||
ApplyCurrentScene();
|
||||
RenderWeather();
|
||||
@@ -361,6 +364,8 @@ public abstract class WeatherWidgetBase : UserControl,
|
||||
}
|
||||
|
||||
CurrentVisualStyleId = WeatherVisualStyleCatalog.Normalize(_settingsFacade.Weather.Get().IconPackId);
|
||||
CurrentPalette = MaterialWeatherVisualTheme.ResolvePalette(CurrentVisualStyleId, CurrentCondition, CurrentIsNight);
|
||||
ApplyCurrentScene();
|
||||
RenderWeather();
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,18 @@
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
<Button x:Name="SurfaceModeButton"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Padding="0"
|
||||
CornerRadius="15"
|
||||
ToolTip.Tip="Full screen"
|
||||
Click="OnSurfaceModeButtonClick">
|
||||
<fi:SymbolIcon x:Name="SurfaceModeIcon"
|
||||
Symbol="ArrowExport"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -16,6 +16,7 @@ using Avalonia.Threading;
|
||||
using DotNetCampus.Inking;
|
||||
using DotNetCampus.Inking.Primitive;
|
||||
using FluentIcons.Avalonia;
|
||||
using FluentIcons.Common;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
@@ -23,6 +24,12 @@ using SkiaSharp;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public enum WhiteboardWidgetSurfaceMode
|
||||
{
|
||||
Component,
|
||||
AirApp
|
||||
}
|
||||
|
||||
public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IComponentPlacementContextAware, IDisposable
|
||||
{
|
||||
private enum WhiteboardToolMode
|
||||
@@ -64,6 +71,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
private bool _noteDirty;
|
||||
private int _noteSaveRevision;
|
||||
private int _noteLoadRevision;
|
||||
private WhiteboardWidgetSurfaceMode _surfaceMode = WhiteboardWidgetSurfaceMode.Component;
|
||||
private Action? _airAppCloseAction;
|
||||
private bool _disposed;
|
||||
|
||||
public WhiteboardWidget()
|
||||
@@ -190,7 +199,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(toolbarPaddingVertical, 4, 8));
|
||||
ToolbarButtonsPanel.Spacing = toolbarSpacing;
|
||||
|
||||
foreach (var button in new[] { PenButton, EraserButton, HandButton, ClearButton, FileButton })
|
||||
foreach (var button in new[] { PenButton, EraserButton, HandButton, ClearButton, FileButton, SurfaceModeButton })
|
||||
{
|
||||
button.Width = buttonSize;
|
||||
button.Height = buttonSize;
|
||||
@@ -274,6 +283,13 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
SchedulePersistedNoteLoad();
|
||||
}
|
||||
|
||||
public void SetSurfaceMode(WhiteboardWidgetSurfaceMode mode, Action? airAppCloseAction = null)
|
||||
{
|
||||
_surfaceMode = mode;
|
||||
_airAppCloseAction = airAppCloseAction;
|
||||
RefreshSurfaceModeButton();
|
||||
}
|
||||
|
||||
public void RefreshFromSettings()
|
||||
{
|
||||
try
|
||||
@@ -475,6 +491,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
ApplyToolButtonVisual(HandButton, _toolMode == WhiteboardToolMode.PanZoom, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
ApplyToolButtonVisual(ClearButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
ApplyToolButtonVisual(FileButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
ApplyToolButtonVisual(SurfaceModeButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
|
||||
}
|
||||
|
||||
private static void ApplyToolButtonVisual(
|
||||
@@ -553,6 +570,42 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
QueueNoteSave();
|
||||
}
|
||||
|
||||
private void OnSurfaceModeButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_surfaceMode == WhiteboardWidgetSurfaceMode.AirApp)
|
||||
{
|
||||
ForceSaveNote();
|
||||
_airAppCloseAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasValidPersistenceContext())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AirAppLauncherServiceProvider
|
||||
.GetOrCreate()
|
||||
.OpenWhiteboard(_componentId, _placementId);
|
||||
}
|
||||
|
||||
private void RefreshSurfaceModeButton()
|
||||
{
|
||||
if (SurfaceModeIcon is not null)
|
||||
{
|
||||
SurfaceModeIcon.Symbol = _surfaceMode == WhiteboardWidgetSurfaceMode.AirApp
|
||||
? Symbol.Subtract
|
||||
: Symbol.ArrowExport;
|
||||
}
|
||||
|
||||
if (SurfaceModeButton is not null)
|
||||
{
|
||||
ToolTip.SetTip(
|
||||
SurfaceModeButton,
|
||||
_surfaceMode == WhiteboardWidgetSurfaceMode.AirApp ? "Exit" : "Full screen");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewportPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (_toolMode != WhiteboardToolMode.PanZoom)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
@@ -13,7 +14,11 @@ using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware
|
||||
public partial class WorldClockWidget : UserControl,
|
||||
IDesktopComponentWidget,
|
||||
ITimeZoneAwareComponentWidget,
|
||||
IComponentPlacementContextAware,
|
||||
IComponentRuntimeContextAware
|
||||
{
|
||||
private const int BaseWidthCells = 4;
|
||||
private const int BaseHeightCells = 2;
|
||||
@@ -106,6 +111,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
private bool _isNightVisual = true;
|
||||
private string _componentId = BuiltInComponentIds.DesktopWorldClock;
|
||||
private string _placementId = string.Empty;
|
||||
private DesktopComponentRenderMode _renderMode = DesktopComponentRenderMode.Live;
|
||||
|
||||
public WorldClockWidget()
|
||||
{
|
||||
@@ -122,6 +128,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
PointerReleased += OnPointerReleased;
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
@@ -159,6 +166,15 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
|
||||
{
|
||||
_componentId = string.IsNullOrWhiteSpace(context.ComponentId)
|
||||
? BuiltInComponentIds.DesktopWorldClock
|
||||
: context.ComponentId.Trim();
|
||||
_placementId = context.PlacementId?.Trim() ?? string.Empty;
|
||||
_renderMode = context.RenderMode;
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
@@ -316,6 +332,20 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
UpdateClockVisuals();
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
if (e.InitialPressMouseButton != MouseButton.Left ||
|
||||
_renderMode != DesktopComponentRenderMode.Live ||
|
||||
!string.Equals(_componentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AirAppLauncherServiceProvider.GetOrCreate().OpenWorldClock(_placementId);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void BuildClockEntryVisuals()
|
||||
{
|
||||
ClockHostGrid.Children.Clear();
|
||||
|
||||
@@ -15,6 +15,7 @@ public partial class DesktopWidgetWindow : Window
|
||||
public DesktopWidgetWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
AppLogger.Info("DesktopWidgetWindow", "Initialized. WindowRole=DesktopSurface.");
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
@@ -44,15 +45,23 @@ public partial class DesktopWidgetWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshDesktopLayer()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || !IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bottomMostService.SendToBottom(this);
|
||||
Dispatcher.UIThread.Post(UpdateInteractiveRegion, DispatcherPriority.Render);
|
||||
AppLogger.Info("DesktopWidgetWindow", "Refreshed desktop layer. WindowRole=DesktopSurface.");
|
||||
}
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_bottomMostService.SendToBottom(this);
|
||||
Dispatcher.UIThread.Post(UpdateInteractiveRegion, DispatcherPriority.Render);
|
||||
}
|
||||
RefreshDesktopLayer();
|
||||
}
|
||||
|
||||
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
||||
@@ -72,4 +81,14 @@ public partial class DesktopWidgetWindow : Window
|
||||
new(0, 0, Bounds.Width, Bounds.Height)
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
if (ComponentContainer.Child is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
ComponentContainer.Child = null;
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||
"all",
|
||||
L(languageCode, "component_category.all", "All"),
|
||||
Symbol.Apps,
|
||||
Icon.Apps,
|
||||
Array.Empty<ComponentLibraryItemViewModel>()));
|
||||
|
||||
var usedCategories = _allDefinitions
|
||||
@@ -97,28 +97,18 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
.Select(definition => CreateComponentItem(definition, languageCode))
|
||||
.ToArray();
|
||||
|
||||
var categoryDefinitions = _allDefinitions
|
||||
.Where(definition => string.Equals(definition.Category, category, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||
category,
|
||||
GetLocalizedCategoryTitle(languageCode, category),
|
||||
ResolveCategoryIcon(category),
|
||||
ComponentCategoryIconResolver.ResolveCategoryIcon(category, categoryDefinitions),
|
||||
categoryComponents));
|
||||
}
|
||||
}
|
||||
|
||||
private static Symbol ResolveCategoryIcon(string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return Symbol.Clock;
|
||||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) return Symbol.CalendarDate;
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase)) return Symbol.WeatherSunny;
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase)) return Symbol.Edit;
|
||||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase)) return Symbol.Play;
|
||||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) return Symbol.Info;
|
||||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) return Symbol.Calculator;
|
||||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase)) return Symbol.Hourglass;
|
||||
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase)) return Symbol.Folder;
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
private string GetLocalizedCategoryTitle(string languageCode, string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.clock", "Clock");
|
||||
|
||||
@@ -58,7 +58,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private sealed record ComponentLibraryCategory(
|
||||
string Id,
|
||||
Symbol Icon,
|
||||
Icon Icon,
|
||||
string Title,
|
||||
IReadOnlyList<ComponentLibraryComponentEntry> Components);
|
||||
|
||||
@@ -2873,7 +2873,13 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!_isComponentLibraryOpen || HasActiveDesktopEditSession)
|
||||
if (!_isComponentLibraryOpen)
|
||||
{
|
||||
TryOpenAirAppFromDesktopComponent(sender, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasActiveDesktopEditSession)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -2917,6 +2923,29 @@ public partial class MainWindow : Window
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void TryOpenAirAppFromDesktopComponent(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (HasActiveDesktopEditSession ||
|
||||
DesktopPagesViewport is null ||
|
||||
sender is not Border host ||
|
||||
host.Tag is not string placementId ||
|
||||
!e.GetCurrentPoint(host).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
|
||||
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||||
if (placement is null ||
|
||||
!string.Equals(placement.ComponentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_airAppLauncherService.OpenWorldClock(placement.PlacementId);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void SetSelectedDesktopComponent(Border? host)
|
||||
{
|
||||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||||
@@ -3390,9 +3419,9 @@ public partial class MainWindow : Window
|
||||
var row = new RowDefinition(GridLength.Auto);
|
||||
ComponentLibraryCategoryPagesContainer.RowDefinitions.Add(row);
|
||||
|
||||
var icon = new SymbolIcon
|
||||
var icon = new FluentIcon
|
||||
{
|
||||
Symbol = category.Icon,
|
||||
Icon = category.Icon,
|
||||
IconVariant = IconVariant.Regular,
|
||||
FontSize = 18,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
@@ -3461,62 +3490,14 @@ public partial class MainWindow : Window
|
||||
return categories
|
||||
.Select(category => new ComponentLibraryCategory(
|
||||
category.Id,
|
||||
ResolveComponentLibraryCategoryIcon(category.Id),
|
||||
ComponentCategoryIconResolver.ResolveCategoryIcon(
|
||||
category.Id,
|
||||
_componentRegistry.GetAll().Where(d => string.Equals(d.Category, category.Id, StringComparison.OrdinalIgnoreCase))),
|
||||
GetLocalizedComponentLibraryCategoryTitle(category.Id),
|
||||
category.Components))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private Symbol ResolveComponentLibraryCategoryIcon(string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Clock;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.CalendarDate;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.WeatherSunny;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Edit;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Play;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Calculator;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Hourglass;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Folder;
|
||||
}
|
||||
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
private string GetLocalizedComponentLibraryCategoryTitle(string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -106,6 +106,7 @@ public partial class MainWindow : Window
|
||||
private readonly IComponentLibraryService _componentLibraryService;
|
||||
private readonly IComponentEditorWindowService _componentEditorWindowService;
|
||||
private readonly IEmbeddedComponentLibraryService _componentLibraryWindowService = new EmbeddedComponentLibraryService();
|
||||
private readonly IAirAppLauncherService _airAppLauncherService = AirAppLauncherServiceProvider.GetOrCreate();
|
||||
private ComponentLibraryWindow? _detachedComponentLibraryWindow;
|
||||
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
|
||||
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.TransparentOverlayWindow"
|
||||
WindowDecorations="None"
|
||||
CanResize="False"
|
||||
@@ -30,6 +31,40 @@
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveDockGlassBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusIsland}" />
|
||||
<Setter Property="BoxShadow" Value="0 8 32 #33000000" />
|
||||
</Style>
|
||||
<Style Selector="Button.edit-toolbar-button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}" />
|
||||
<Setter Property="Padding" Value="14,8" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="Button.edit-toolbar-button:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
|
||||
<Setter Property="RenderTransform" Value="scale(1.02)" />
|
||||
</Style>
|
||||
<Style Selector="Button.edit-toolbar-button:pressed">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonPressedBackgroundBrush}" />
|
||||
<Setter Property="RenderTransform" Value="scale(0.98)" />
|
||||
</Style>
|
||||
<Style Selector="Button.edit-toolbar-button fi|FluentIcon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="Border.edit-toolbar-separator">
|
||||
<Setter Property="Width" Value="1" />
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
<Setter Property="Margin" Value="4,8" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
@@ -43,18 +78,23 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,20"
|
||||
Padding="8"
|
||||
Padding="6"
|
||||
IsHitTestVisible="True">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button MinWidth="112"
|
||||
Padding="16,8"
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<Button Classes="edit-toolbar-button"
|
||||
Click="OnRestoreComponentLibraryClick">
|
||||
<TextBlock Text="找回组件库" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:FluentIcon Icon="Apps" IconVariant="Regular" />
|
||||
<TextBlock Text="找回组件库" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button MinWidth="96"
|
||||
Padding="16,8"
|
||||
<Border Classes="edit-toolbar-separator" />
|
||||
<Button Classes="edit-toolbar-button"
|
||||
Click="OnExitEditClick">
|
||||
<TextBlock Text="退出编辑" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:FluentIcon Icon="Dismiss" IconVariant="Regular" />
|
||||
<TextBlock Text="退出编辑" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -224,17 +224,27 @@ public partial class TransparentOverlayWindow : Window
|
||||
_layout = _layoutService.Load();
|
||||
RenderAllComponents();
|
||||
|
||||
AppLogger.Info("TransparentOverlay", $"Opened with {_layout.ComponentPlacements.Count} components.");
|
||||
AppLogger.Info(
|
||||
"TransparentOverlay",
|
||||
$"Opened with {_layout.ComponentPlacements.Count} components. WindowRole=DesktopSurface.");
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_bottomMostService.SendToBottom(this);
|
||||
}
|
||||
RefreshDesktopLayer();
|
||||
|
||||
Dispatcher.UIThread.Post(UpdateInteractiveRegions, DispatcherPriority.Background);
|
||||
DispatcherTimer.RunOnce(LogTransparencyDiagnostics, TimeSpan.FromMilliseconds(250));
|
||||
}
|
||||
|
||||
public void RefreshDesktopLayer()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || !IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bottomMostService.SendToBottom(this);
|
||||
AppLogger.Info("TransparentOverlay", "Refreshed desktop layer. WindowRole=DesktopSurface.");
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
SaveLayout();
|
||||
|
||||
Reference in New Issue
Block a user