feat.airapp与融合桌面

This commit is contained in:
lincube
2026-05-14 19:44:01 +08:00
parent ada0cd4a3a
commit a5abda62dc
64 changed files with 3617 additions and 362 deletions

View File

@@ -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;
}
}

View File

@@ -107,4 +107,32 @@
<Exec Command="powershell -ExecutionPolicy Bypass -File &quot;$(MSBuildProjectDirectory)\..\scripts\Generate-VersionFile.ps1&quot; -OutputPath &quot;$(VersionFilePath)&quot; -Version &quot;$(AppVersion)&quot; -Codename &quot;$(AppCodename)&quot;" Condition="'$(OS)' == 'Windows_NT'" />
<Exec Command="pwsh -ExecutionPolicy Bypass -File &quot;$(MSBuildProjectDirectory)\..\scripts\Generate-VersionFile.ps1&quot; -OutputPath &quot;$(VersionFilePath)&quot; -Version &quot;$(AppVersion)&quot; -Codename &quot;$(AppCodename)&quot;" Condition="'$(OS)' != 'Windows_NT'" />
</Target>
<Target Name="BuildAirAppHostOutput"
AfterTargets="Build"
Condition="'$(BuildingAirAppHost)' != 'true' and '$(SkipAirAppHostBuild)' != 'true'">
<Exec Command="dotnet build &quot;$(MSBuildProjectDirectory)\..\LanMountainDesktop.AirAppHost\LanMountainDesktop.AirAppHost.csproj&quot; -c &quot;$(Configuration)&quot; --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>

View 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;
}
}
}

View File

@@ -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)

View File

@@ -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; }
}

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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");

View File

@@ -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))

View File

@@ -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);

View File

@@ -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>

View File

@@ -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();